Skip to content

Commit

Permalink
Add test_code to output (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
senekor authored Aug 19, 2024
1 parent 93936a6 commit 9c4b6c1
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 9 deletions.
9 changes: 8 additions & 1 deletion bin/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ exit_code=0
for test_dir in tests/*; do
test_dir_name=$(basename "${test_dir}")
test_dir_path=$(realpath "${test_dir}")
if [ -d "$test_dir_path/tests" ] ; then
test_file_path=$test_dir_path/tests/$(ls "$test_dir_path/tests/")
else
test_file_path=$test_dir_path
fi
test_file_name=$(basename "$test_file_path")
slug=${test_file_name%.*}
results_file_path="${test_dir_path}/results.json"
expected_results_file_path="${test_dir_path}/expected_results.json"

# Remove any build caches
rm -rf "${test_dir_path}/target"
rm -f "${test_dir_path}/Cargo.lock"

bin/run.sh "${test_dir_name}" "${test_dir_path}" "${test_dir_path}"
bin/run.sh "${slug}" "${test_dir_path}" "${test_dir_path}"

# Normalize the results file
jq 'if (.tests != null) then .tests |= sort_by(.name) else . end' "${results_file_path}" > tmp && mv tmp "${results_file_path}"
Expand Down
28 changes: 23 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
pub mod cargo_test;
pub mod cli;
pub mod output;
pub mod parse_test_code;
pub mod test_name_formatter;

use std::collections::HashMap;

use cargo_test as ct;
use output as o;

/// convert a stream of test events into a single test result
pub fn convert<I, E>(events: I) -> o::Output
pub fn convert<I, E>(events: I, name_to_code: HashMap<String, String>) -> o::Output
where
I: Iterator<Item = Result<ct::TestEvent, E>>,
E: serde::de::Error + std::fmt::Display,
Expand Down Expand Up @@ -38,6 +41,11 @@ where
break;
}
};
let test_code = name_to_code
.get(&name)
.map(String::as_str)
.unwrap_or(TEST_CODE_NOT_FOUND_MSG)
.to_string();
match event.event {
ct::Event::Started => continue,
ct::Event::Ok => {
Expand All @@ -46,12 +54,13 @@ where
out.status = o::Status::Pass;
}
out.message = None;
out.tests.push(o::TestResult::ok(name));
out.tests.push(o::TestResult::ok(name, test_code));
}
ct::Event::Failed => {
out.status = o::Status::Fail;
out.message = None;
out.tests.push(o::TestResult::fail(name, event.stdout));
out.tests
.push(o::TestResult::fail(name, test_code, event.stdout));
}
ct::Event::Ignored => {
out.status = o::Status::Error;
Expand All @@ -63,6 +72,13 @@ where
out
}

static TEST_CODE_NOT_FOUND_MSG: &str = "\
It looks like the test runner failed to retrieve the code for this test. \
Please consider reporting this on the forum so we can try to fix it. \
Thanks!
https://forum.exercism.org/c/programming/rust/112";

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -80,8 +96,10 @@ mod test {

#[test]
fn test_convert() {
let out =
convert(serde_json::Deserializer::from_str(TEST_DATA).into_iter::<ct::TestEvent>());
let out = convert(
serde_json::Deserializer::from_str(TEST_DATA).into_iter::<ct::TestEvent>(),
HashMap::new(),
);
assert_eq!(out.status, o::Status::Fail);
for test in out.tests {
if test.name == "Test::fail" {
Expand Down
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use clap::Parser;
use regex::Regex;
use rust_test_runner::cargo_test::TestEvent;
use rust_test_runner::cli::CliArgs;
use rust_test_runner::convert;
use rust_test_runner::{convert, parse_test_code};
use serde_json as json;

fn main() -> Result<()> {
Expand Down Expand Up @@ -70,8 +70,15 @@ fn main() -> Result<()> {

results_out.push_str(&deterministic_cargo_stderr);

// if there is no test file at the standard location (tests/<slug>.rs),
// pretend like the test file is empty
let test_file =
std::fs::read_to_string(format!("tests/{}.rs", cli_args.slug)).unwrap_or_default();
let name_to_code = parse_test_code::parse_file(&test_file);

let out = convert(
serde_json::Deserializer::from_slice(&cargo_output.stdout).into_iter::<TestEvent>(),
name_to_code,
);
let mut results_json = serde_json::to_string_pretty(&out)?;

Expand Down
7 changes: 5 additions & 2 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,25 @@ pub enum Status {
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TestResult {
pub name: String,
pub test_code: String,
pub status: Status,
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}

impl TestResult {
pub fn ok(name: String) -> TestResult {
pub fn ok(name: String, test_code: String) -> TestResult {
TestResult {
name: format_test_name(name),
test_code,
status: Status::Pass,
message: None,
output: None,
}
}

pub fn fail(name: String, message: Option<String>) -> TestResult {
pub fn fail(name: String, test_code: String, message: Option<String>) -> TestResult {
let name = format_test_name(name);

let (output, message) = match message.as_ref().and_then(|m| m.split_once("thread '")) {
Expand All @@ -52,6 +54,7 @@ impl TestResult {

TestResult {
name,
test_code,
message,
status: Status::Fail,
output,
Expand Down
170 changes: 170 additions & 0 deletions src/parse_test_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::collections::HashMap;

pub fn parse_file(test_file: &str) -> HashMap<String, String> {
let mut name_to_code = HashMap::new();
let mut lines = test_file.lines();
while let Some(line) = lines.by_ref().next() {
if line.contains("#[test]") {
let (name, code) = parse_function(&mut lines);
name_to_code.insert(name, code);
}
}
name_to_code
}

fn parse_function(lines: &mut std::str::Lines) -> (String, String) {
let mut should_panic = None;
while let Some(line) = lines.next() {
if line.contains("#[ignore]") {
continue;
}
if line.contains("#[should_panic") {
should_panic = Some(line.trim());
}
if let Some((indent, signature)) = line.split_once("fn ") {
let indent = format!("{indent} "); // plus 4 spaces
let name = signature.split(['<', '(']).next().unwrap().to_string();
let mut body = parse_body(lines, indent);
if let Some(should_panic) = should_panic {
body = format!("// {should_panic}\n{body}");
}
return (name, body);
}
}
panic!("did not find function definition")
}

fn parse_body(lines: &mut std::str::Lines, indent: String) -> String {
let mut res = String::new();
let function_end = format!("{}}}", &indent[4..]);
for line in lines {
if line.starts_with(&function_end) {
res.pop(); // trailing newline
return res;
}
res.push_str(line.strip_prefix(&indent).unwrap_or(line));
res.push('\n');
}
panic!("end of function body not found")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn simple() {
let input = "\
#[test]
fn foo() {
first_line();
second_line();
}";
let expected = "\
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn ignore() {
let input = "\
#[test]
#[ignore]
fn foo() {
first_line();
second_line();
}";
let expected = "\
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn indented() {
let input = "\
#[test]
fn foo() {
first_line();
second_line();
}";
let expected = "\
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn should_panic() {
let input = "\
#[test]
#[should_panic]
fn foo() {
first_line();
second_line();
}";
let expected = "\
// #[should_panic]
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn should_panic_with_msg() {
let input = "\
#[test]
#[should_panic(\"reason\")]
fn foo() {
first_line();
second_line();
}";
let expected = "\
// #[should_panic(\"reason\")]
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn more_indentation() {
let input = "\
#[test]
fn foo() {
first_line();
if condition {
indented_line();
}
}";
let expected = "\
first_line();
if condition {
indented_line();
}";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}

#[test]
fn empty_line() {
let input = "\
#[test]
fn foo() {
first_line();
second_line();
}";
let expected = "\
first_line();
second_line();";
let name_to_code = parse_file(input);
assert_eq!(name_to_code["foo"], expected);
}
}
Loading

0 comments on commit 9c4b6c1

Please sign in to comment.