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
24pub const SETUP_RTT_TIMEOUT_SEC: u64 = 60;
26pub const EXECUTION_TIMEOUT_SEC: u64 = 3600; #[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 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 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 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 end_signal.store(true, std::sync::atomic::Ordering::Relaxed);
472
473 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
484pub 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}