embedded_runner/
lib.rs

1use std::{
2    net::{Ipv4Addr, SocketAddrV4, TcpStream},
3    path::{Path, PathBuf},
4    process::Stdio,
5    sync::{atomic::AtomicBool, Arc},
6};
7
8use cfg::{CliConfig, ResolvedConfig, RunCmdConfig, RunnerConfig};
9use covcon::cfg::DataFormat;
10use coverage::CoverageError;
11use defmt_json_schema::v1::JsonFrame;
12use path_clean::PathClean;
13use serde_json::json;
14use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter};
15
16pub mod cfg;
17pub mod collect;
18pub mod coverage;
19pub mod defmt;
20pub mod path;
21
22pub const DEFAULT_RTT_PORT: u16 = 19021;
23
24/// Timeout defines the maximum duration to setup RTT connection between host and target
25pub const SETUP_RTT_TIMEOUT_SEC: u64 = 60;
26/// Timeout defines the maximum duration of one test run
27pub const EXECUTION_TIMEOUT_SEC: u64 = 3600; // 1h
28
29#[derive(Debug, thiserror::Error)]
30pub enum RunnerError {
31    #[error("Timeout waiting for rtt connection to start.")]
32    RttTimeout,
33    #[error("Error from gdb: {}", .0)]
34    Gdb(String),
35    #[error("Error setting up the gdb script: {}", .0)]
36    GdbScript(String),
37    #[error("{}", .0)]
38    Config(#[from] cfg::ConfigError),
39    #[error("Error reading defmt logs: {}", .0)]
40    Defmt(String),
41    #[error("Failed setting up embedded-runner. Cause: {}", .0)]
42    Setup(String),
43    #[error("Failed executing pre runner. Cause: {}", .0)]
44    PreRunner(String),
45    #[error("Failed executing post runner. Cause: {}", .0)]
46    PostRunner(String),
47    #[error("Could not create coverage data. Cause: {}", .0)]
48    Coverage(CoverageError),
49}
50
51pub async fn run(cli_cfg: CliConfig) -> Result<(), RunnerError> {
52    match cli_cfg.cmd {
53        cfg::Cmd::Run(run_cfg) => {
54            let cfg = cfg::get_cfg(&run_cfg.runner_cfg, cli_cfg.verbose)?;
55            run_cmd(&cfg, run_cfg).await
56        }
57        cfg::Cmd::Collect(collect_cfg) => collect::run(collect_cfg).await,
58    }
59}
60
61pub async fn run_cmd(main_cfg: &ResolvedConfig, run_cfg: RunCmdConfig) -> Result<(), RunnerError> {
62    let output_dir = match run_cfg.output_dir {
63        Some(dir) => dir,
64        None => {
65            let mut dir = run_cfg.binary.clone();
66            dir.set_file_name(format!(
67                "{}_runner",
68                run_cfg
69                    .binary
70                    .file_name()
71                    .expect("Binary name must be a valid filename.")
72                    .to_string_lossy()
73            ));
74            dir
75        }
76    };
77
78    if !output_dir.exists() {
79        tokio::fs::create_dir_all(&output_dir)
80            .await
81            .map_err(|err| {
82                RunnerError::Setup(format!(
83                    "Could not create directory '{}'. Cause: {}",
84                    output_dir.display(),
85                    err
86                ))
87            })?;
88    }
89
90    let log_filepath = output_dir.join("defmt.log");
91
92    let binary_str = run_cfg.binary.display().to_string();
93    let rel_binary_path = run_cfg
94        .binary
95        .strip_prefix(
96            crate::path::get_cargo_root().unwrap_or(std::env::current_dir().unwrap_or_default()),
97        )
98        .map(|p| p.to_path_buf())
99        .unwrap_or(run_cfg.binary.clone());
100    let rel_binary_str = rel_binary_path.display().to_string();
101
102    #[cfg(target_os = "windows")]
103    let pre_command = main_cfg
104        .runner_cfg
105        .pre_runner_windows
106        .as_ref()
107        .or(main_cfg.runner_cfg.pre_runner.as_ref());
108    #[cfg(not(target_os = "windows"))]
109    let pre_command = main_cfg.runner_cfg.pre_runner.as_ref();
110
111    if let Some(pre_command) = pre_command {
112        println!("-------------------- Pre Runner --------------------");
113        let mut args = pre_command.args.clone();
114        args.push(binary_str.clone());
115
116        let output = tokio::process::Command::new(&pre_command.name)
117            .args(args)
118            .current_dir(&main_cfg.workspace_dir)
119            .output()
120            .await
121            .map_err(|err| RunnerError::PreRunner(err.to_string()))?;
122        print!(
123            "{}",
124            String::from_utf8(output.stdout).expect("Stdout must be valid utf8.")
125        );
126        eprint!(
127            "{}",
128            String::from_utf8(output.stderr).expect("Stderr must be valid utf8.")
129        );
130
131        if !output.status.success() {
132            return Err(RunnerError::PreRunner(format!(
133                "Returned with exit code: {}",
134                output.status,
135            )));
136        }
137    }
138
139    let gdb_script = main_cfg
140        .runner_cfg
141        .gdb_script(
142            &run_cfg.binary,
143            &output_dir,
144            run_cfg.segger_gdb.unwrap_or(main_cfg.runner_cfg.segger_gdb),
145        )
146        .map_err(|_err| RunnerError::GdbScript(String::new()))?;
147
148    let gdb_script_file = output_dir.join("embedded.gdb");
149    tokio::fs::write(&gdb_script_file, gdb_script)
150        .await
151        .map_err(|err| RunnerError::GdbScript(err.to_string()))?;
152
153    let (defmt_frames, gdb_result) = run_gdb_sequence(
154        run_cfg.binary,
155        &main_cfg.workspace_dir,
156        &gdb_script_file,
157        &main_cfg.runner_cfg,
158    )
159    .await?;
160    let gdb_status = gdb_result?;
161
162    if !gdb_status.success() {
163        return Err(RunnerError::Gdb(format!(
164            "GDB did not run successfully. Exit code: '{gdb_status}'"
165        )));
166    }
167
168    println!("------------------ Output ------------------");
169
170    if defmt_frames.is_empty() {
171        println!("No logs received.");
172    } else {
173        let log_file = tokio::fs::File::create(&log_filepath)
174            .await
175            .map_err(|err| {
176                RunnerError::Setup(format!(
177                    "Could not create file '{}'. Cause: {}",
178                    log_filepath.display(),
179                    err
180                ))
181            })?;
182        let mut writer = BufWriter::new(log_file);
183
184        for frame in &defmt_frames {
185            let _w = writer
186                .write_all(
187                    serde_json::to_string(frame)
188                        .expect("DefmtFrame is valid JSON.")
189                        .as_bytes(),
190                )
191                .await;
192            let _w = writer.write_all("\n".as_bytes()).await;
193        }
194
195        let _f = writer.flush().await;
196
197        println!("Logs written to '{}'.", log_filepath.display());
198
199        let run_name = run_cfg
200            .run_name
201            .unwrap_or(rel_binary_path.display().to_string());
202
203        let data_path = run_cfg
204            .data_filepath
205            .or(main_cfg.runner_cfg.data_filepath.clone())
206            .unwrap_or(main_cfg.embedded_dir.join("test_run_data.json"));
207
208        let mut data = if data_path.exists() {
209            let data_content = tokio::fs::read_to_string(&data_path).await.map_err(|err| {
210                RunnerError::Setup(format!(
211                    "Could not read custom test run data '{}'. Cause: {}",
212                    data_path.display(),
213                    err
214                ))
215            })?;
216
217            let mut data: serde_json::Map<String, serde_json::Value> =
218                serde_json::from_str(&data_content).map_err(|err| {
219                    RunnerError::Setup(format!(
220                        "Could not deserialize metadata '{}'. Cause: {}",
221                        data_path.display(),
222                        err
223                    ))
224                })?;
225
226            data.insert(
227                "binary".to_string(),
228                serde_json::Value::String(rel_binary_str),
229            );
230
231            serde_json::Value::Object(data)
232        } else {
233            json!({
234                "binary": rel_binary_str
235            })
236        };
237
238        if let Some(extern_cov) = &main_cfg.runner_cfg.extern_coverage {
239            match (tokio::fs::read_to_string(&extern_cov.filepath).await, covcon::cfg::DataFormat::try_from(extern_cov.filepath.extension())) {
240                (Ok(content), Ok(DataFormat::Xml)) => {
241                    let cov_cfg = covcon::cfg::ConversionConfig {
242                        in_fmt: extern_cov.format,
243                        in_content: content,
244                        in_data_fmt: DataFormat::Xml,
245                        out_fmt: covcon::format::CoverageFormat::CoberturaV4,
246                        out_data_fmt: DataFormat::Json,
247                    };
248
249                    match covcon::convert::convert_to_json(&cov_cfg) {
250                        Ok(json_cov) => {
251                            let meta_map = data.as_object_mut().expect("Meta is created as object above.");
252                            meta_map.insert("coverage".to_string(), json_cov);
253                        },
254                        Err(err) => log::error!("Failed extracting external coverage data. External coverage will be ignored. Cause: {err}"),
255                    }
256                }
257                (Err(err), _) => log::error!("Failed to read external coverage file. External coverage will be ignored. Cause: {err}"),
258                (_, _) => log::error!("Coverage file must be XML. External coverage will be ignored."),
259            }
260        }
261
262        let logs =
263            serde_json::to_string(&defmt_frames).expect("DefmtFrames were deserialized before.");
264
265        let coverage = coverage::coverage_from_defmt_frames(
266            run_name,
267            Some(data),
268            defmt_frames.as_slice(),
269            Some(logs),
270        )
271        .map_err(RunnerError::Coverage)?;
272
273        // If no tests were found, execution most likely `cargo run` or `cargo bench` => no test coverage
274        if coverage
275            .test_runs
276            .iter()
277            .any(|test_run| test_run.nr_of_tests > 0)
278        {
279            let coverage_file = output_dir.join("coverage.json");
280            tokio::fs::write(
281                &coverage_file,
282                serde_json::to_string(&coverage).expect("Coverage schema is valid JSON."),
283            )
284            .await
285            .map_err(|err| {
286                RunnerError::Setup(format!(
287                    "Could not write to file '{}'. Cause: {}",
288                    coverage_file.display(),
289                    err
290                ))
291            })?;
292
293            println!("Coverage written to '{}'.", coverage_file.display());
294
295            let coverages_filepath = coverage::coverages_filepath();
296
297            if !coverages_filepath.exists() {
298                let _w =
299                    tokio::fs::write(coverages_filepath, coverage_file.display().to_string()).await;
300            } else {
301                let mut file = tokio::fs::OpenOptions::new()
302                    .append(true)
303                    .read(true)
304                    .open(coverages_filepath)
305                    .await
306                    .expect("Coverages file exists.");
307
308                let mut content = String::new();
309                file.read_to_string(&mut content)
310                    .await
311                    .expect("Reading coverages");
312
313                let mut exists = false;
314                for line in content.lines() {
315                    if line == coverage_file.display().to_string() {
316                        exists = true;
317                        break;
318                    }
319                }
320
321                if !exists {
322                    let _w = file.write_all("\n".as_bytes()).await;
323                    let _w = file
324                        .write_all(coverage_file.display().to_string().as_bytes())
325                        .await;
326                }
327
328                let _f = file.flush().await;
329            }
330        }
331    }
332
333    #[cfg(target_os = "windows")]
334    let post_command = main_cfg
335        .runner_cfg
336        .post_runner_windows
337        .as_ref()
338        .or(main_cfg.runner_cfg.post_runner.as_ref());
339    #[cfg(not(target_os = "windows"))]
340    let post_command = main_cfg.runner_cfg.post_runner.as_ref();
341
342    if let Some(post_command) = post_command {
343        println!("-------------------- Post Runner --------------------");
344        let mut args = post_command.args.clone();
345        args.push(binary_str);
346
347        let output = tokio::process::Command::new(&post_command.name)
348            .args(args)
349            .current_dir(&main_cfg.workspace_dir)
350            .output()
351            .await
352            .map_err(|err| RunnerError::PostRunner(err.to_string()))?;
353        print!(
354            "{}",
355            String::from_utf8(output.stdout).expect("Stdout must be valid utf8.")
356        );
357        eprint!(
358            "{}",
359            String::from_utf8(output.stderr).expect("Stderr must be valid utf8.")
360        );
361
362        if !output.status.success() {
363            return Err(RunnerError::PostRunner(format!(
364                "Returned with exit code: {}",
365                output.status,
366            )));
367        }
368    }
369
370    Ok(())
371}
372
373pub async fn run_gdb_sequence(
374    binary: PathBuf,
375    workspace_dir: &Path,
376    tmp_gdb_file: &Path,
377    runner_cfg: &RunnerConfig,
378) -> Result<
379    (
380        Vec<JsonFrame>,
381        Result<std::process::ExitStatus, RunnerError>,
382    ),
383    RunnerError,
384> {
385    let mut gdb_cmd = tokio::process::Command::new(
386        std::env::var("GDB").unwrap_or("arm-none-eabi-gdb".to_string()),
387    );
388    let mut gdb = gdb_cmd
389        .args([
390            "-x",
391            &tmp_gdb_file.to_string_lossy(),
392            &binary.to_string_lossy(),
393        ])
394        .current_dir(workspace_dir)
395        .stdout(Stdio::piped())
396        .spawn()
397        .unwrap();
398
399    println!("-------------------- Communication Setup --------------------");
400
401    let rtt_port = runner_cfg.rtt_port.unwrap_or(DEFAULT_RTT_PORT);
402    let stream = tokio::time::timeout(
403        std::time::Duration::from_secs(SETUP_RTT_TIMEOUT_SEC),
404        tokio::spawn(async move {
405            loop {
406                match TcpStream::connect(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), rtt_port)) {
407                    Ok(stream) => {
408                        return Ok(stream);
409                    }
410                    Err(err)
411                        if matches!(
412                            err.kind(),
413                            std::io::ErrorKind::TimedOut | std::io::ErrorKind::ConnectionRefused
414                        ) =>
415                    {
416                        std::thread::sleep(std::time::Duration::from_millis(50));
417                    }
418                    Err(err) => {
419                        return Err(err);
420                    }
421                }
422            }
423        }),
424    )
425    .await;
426
427    let stream = match stream {
428        Ok(Ok(Ok(stream))) => stream,
429        Ok(Ok(Err(io_err))) => {
430            log::error!("Failed to connect to RTT. Cause: {io_err}");
431            let _ = gdb.kill().await;
432            return Err(RunnerError::RttTimeout);
433        }
434        _ => {
435            log::error!("Timeout while trying to connect to RTT.");
436            let _ = gdb.kill().await;
437            return Err(RunnerError::RttTimeout);
438        }
439    };
440
441    println!();
442    println!("-------------------- Running --------------------");
443
444    // start defmt thread + end-signal
445    let end_signal = Arc::new(AtomicBool::new(false));
446    let thread_signal = end_signal.clone();
447    let workspace_root = workspace_dir.to_path_buf();
448    let defmt_thread = tokio::spawn(async move {
449        defmt::read_defmt_frames(&binary, &workspace_root, stream, thread_signal)
450    });
451
452    // wait for gdb to end
453    let gdb_result = match tokio::time::timeout(
454        std::time::Duration::from_secs(EXECUTION_TIMEOUT_SEC),
455        gdb.wait(),
456    )
457    .await
458    {
459        Ok(Ok(status)) => Ok(status),
460        Ok(Err(err)) => Err(RunnerError::Gdb(format!(
461            "Error waiting for gdb to finish. Cause: {err}"
462        ))),
463        Err(_) => {
464            log::error!("Timeout while waiting for gdb to end.");
465            let _ = gdb.kill().await;
466            return Err(RunnerError::RttTimeout);
467        }
468    };
469
470    // signal defmt end
471    end_signal.store(true, std::sync::atomic::Ordering::Relaxed);
472
473    // join defmt thread to get logs
474    let defmt_result = defmt_thread
475        .await
476        .map_err(|_| RunnerError::Defmt("Failed waiting for defmt logs.".to_string()))?;
477
478    let defmt_frames = defmt_result
479        .map_err(|err| RunnerError::Defmt(format!("Failed extracting defmt logs. Cause: {err}")))?;
480
481    Ok((defmt_frames, gdb_result))
482}
483
484/// Converts the given path into a cleaned absolute path.
485/// see: https://stackoverflow.com/questions/30511331/getting-the-absolute-path-from-a-pathbuf
486pub fn absolute_path(path: &Path) -> std::io::Result<PathBuf> {
487    let absolute_path = if path.is_absolute() {
488        path.to_path_buf()
489    } else {
490        crate::path::get_cargo_root()
491            .or_else(|_| std::env::current_dir())?
492            .join(path)
493    }
494    .clean();
495
496    Ok(absolute_path)
497}