Skip to content

Commit

Permalink
drop dep on elmi on path
Browse files Browse the repository at this point in the history
Instead we use the approach of
<rtfeldman/node-test-runner#442> from which I
have taken a lot of inspiration for this commit.
  • Loading branch information
harrysarson committed Oct 31, 2020
1 parent d3aec28 commit ba6b277
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 80 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ rand = { version = "0.7.3", default-features = false, features = ["std"] }
num_cpus = "1"
thiserror = "1.0.21"
tree-sitter = "0.17.0"
regex = "1.4.1"
lazy_static = "1.4.0"

[dev-dependencies]

Expand Down
73 changes: 24 additions & 49 deletions src/elmi.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,37 @@
//! Basically a wrapper module for elmi-to-json for the time being.
//! It reads the compiled .elmi files and extracts exposed tests.
use miniserde::{json, Deserialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::fs;

/// Use elmi-to-json as a binary to extract all exposed tests
/// from compiled .elmi files.
pub fn all_tests<P: AsRef<Path>>(
work_dir: P,
src_files: &HashSet<PathBuf>,
use std::path::Path;

/// Find all possible tests (all values) in test_files.
pub fn all_tests(
test_files: impl IntoIterator<Item = impl AsRef<Path>>,
) -> Result<Vec<TestModule>, String> {
let output = Command::new("elmi-to-json")
.arg("--for-elm-test")
.arg("--elm-version")
.arg("0.19.1")
// stdio config
.current_dir(&work_dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()
.expect("command failed to start");
let str_output = std::str::from_utf8(&output.stdout)
.map_err(|_| "Output of elmi-to-json is not valid UTF-8".to_string())?;
let output: ElmiToJsonOutput =
json::from_str(str_output).map_err(|_| "Deserialization error".to_string())?;
Ok(output
.test_modules
test_files
.into_iter()
// Filter out modules with no test
.filter(|m| !m.tests.is_empty())
// Filter out modules not in the list of src_files (must also be canonical)
.filter(|m| {
let path =
work_dir.as_ref().join(&m.path).canonicalize().expect(
"There was an issue when retrieving module paths from elmi-to-json output",
);
src_files.contains(&path)
})
// No need to verify that module names are valid, elm 0.19.1 already verifies that.
// No need to filter exposed since only exposed values are in elmi files.
.collect())
}
.map(|test_file| {
let source = fs::read_to_string(&test_file).unwrap();

#[derive(Deserialize, Debug)]
/// Struct mirroring the json result of elmi-to-json --for-elm-test.
struct ElmiToJsonOutput {
#[serde(rename = "testModules")]
test_modules: Vec<TestModule>,
let tree = {
let mut parser = tree_sitter::Parser::new();
let language = super::parser::tree_sitter_elm();
parser.set_language(language).unwrap();
parser.parse(&source, None).unwrap()
};

crate::parser::get_all_exposed_values(&tree, &source)
.map(|tests| TestModule {
path: test_file.as_ref().to_str().unwrap().to_string(),
tests: tests.iter().map(|s| s.to_string()).collect(),
})
.map_err(|s| s.to_string())
})
.collect()
}

#[derive(Deserialize, Debug)]
/// Test modules as listed in the json result of elmi-to-json.
pub struct TestModule {
#[serde(rename = "moduleName")]
pub module_name: String,
pub path: String,
pub tests: Vec<String>,
}
3 changes: 1 addition & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ fn main_args() -> Result<Args, Box<dyn std::error::Error>> {
/// This happens for example with the command: `elm-test-rs /path/to/some/Module.elm`.
fn no_subcommand_args(
first_arg: Option<String>,
args: pico_args::Arguments,
mut args: pico_args::Arguments,
) -> Result<Args, Box<dyn std::error::Error>> {
let mut args = args;
let mut rng = rand::thread_rng();
Ok(Args::Run(run::Options {
help: args.contains("--help"),
Expand Down
4 changes: 1 addition & 3 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ extern "C" {
}

#[must_use]
#[allow(dead_code)]
pub fn tree_sitter_elm() -> Language {
unsafe { raw_tree_sitter_elm() }
}
Expand All @@ -29,7 +28,6 @@ pub enum ExplicitExposedValuesError<'a> {
///
/// If the elm file is not valid (it will fail `elm make`).
///
#[allow(dead_code)]
pub fn get_all_exposed_values<'a>(
tree: &'a Tree,
source: &'a str,
Expand Down Expand Up @@ -96,7 +94,7 @@ fn get_all_top_level_values<'a>(
if cursor.node().kind() == "value_declaration" {
let mut c1 = ChildCursor::new(&mut cursor)?;
let c2 = ChildCursor::new(c1.child_mut())?;
v.push(dbg!(&source[c2.child().node().byte_range()]));
v.push(&source[c2.child().node().byte_range()]);
}
if next_sibling(&mut cursor).is_err() {
break Ok(v);
Expand Down
134 changes: 113 additions & 21 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
use crate::elm_json::{Config, Dependencies};
use glob::glob;
use std::collections::HashSet;
use std::convert::TryFrom;
use regex::Regex;
use std::ffi::OsStr;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::{collections::HashSet, fs};
use std::{convert::TryFrom, path};

#[derive(Debug)]
/// Options passed as arguments.
Expand Down Expand Up @@ -101,22 +102,28 @@ pub fn main(options: Options) {

// Make src dirs relative to the generated tests root
let tests_root = elm_project_root.join("elm-stuff/tests-0.19.1");
let mut source_directories: Vec<PathBuf> = elm_json_tests
let elm_test_rs_root = crate::utils::elm_test_rs_root().unwrap();
let test_directories: Vec<PathBuf> = elm_json_tests
.source_directories
.iter()
// Add tests/ to the list of source directories
.chain(std::iter::once(&"tests".to_string()))
// Get canonical form
.map(|path| elm_project_root.join(path).canonicalize().unwrap())
// Get path relative to tests_root
.collect();

let source_directories_for_runner: Vec<PathBuf> = test_directories
.iter()
.map(|path| pathdiff::diff_paths(&path, &tests_root).expect("Could not get relative path"))
// Add src/ and elm-test-rs/elm/src/ to the source directories
.chain(vec![
Path::new("src").into(),
elm_test_rs_root.join("elm/src"),
])
.collect();

// Add src/ and elm-test-rs/elm/src/ to the source directories
let elm_test_rs_root = crate::utils::elm_test_rs_root().unwrap();
source_directories.push(Path::new("src").into());
source_directories.push(elm_test_rs_root.join("elm/src"));
elm_json_tests.source_directories = source_directories
elm_json_tests.source_directories = source_directories_for_runner
.iter()
.map(|path| path.to_str().unwrap().to_string())
.collect();
Expand Down Expand Up @@ -172,34 +179,40 @@ pub fn main(options: Options) {

// Find all modules and tests
eprintln!("Finding all modules and tests ...");
let all_modules_and_tests = crate::elmi::all_tests(&tests_root, &module_paths).unwrap();
let runner_imports: Vec<String> = all_modules_and_tests
.iter()
.map(|m| "import ".to_string() + &m.module_name)
.collect();
let runner_tests: Vec<String> = all_modules_and_tests
let all_modules_and_tests = crate::elmi::all_tests(&module_paths).unwrap();

let (runner_imports, maybe_runner_tests): (Vec<String>, Vec<String>) = all_modules_and_tests
.iter()
.map(|module| {
let module_name = get_module_name(&test_directories, &module.path);
let full_module_tests: Vec<String> = module
.tests
.iter()
.map(move |test| module.module_name.clone() + "." + test)
.map(|test| format!("check {}.{}", &module_name, test))
.collect();
format!(
r#"Test.describe "{}" [ {} ]"#,
&module.module_name,
full_module_tests.join(", ")
let maybe_test = format!(
r#"
{{ module_ = "{}"
, maybeTests =
[ {}
]
}}"#,
&module_name,
full_module_tests.join("\n , ")
)
.trim()
.to_string();
("import ".to_string() + &module_name, maybe_test)
})
.collect();
.unzip();

// Generate templated src/Runner.elm
create_templated(
elm_test_rs_root.join("templates/Runner.elm"), // template
tests_root.join("src/Runner.elm"), // output
vec![
("user_imports".to_string(), runner_imports.join("\n")),
("tests".to_string(), runner_tests.join("\n , ")),
("tests".to_string(), maybe_runner_tests.join("\n , ")),
],
);

Expand All @@ -213,6 +226,14 @@ pub fn main(options: Options) {
&["src/Runner.elm"], // src
);

fs::write(
&compiled_elm_file,
&add_kernel_test_checking(
&fs::read_to_string(&compiled_elm_file).expect("Cannot read newly created elm.js file"),
),
)
.expect("Cannot write updated elm.js file");

// Generate the node_runner.js node module embedding the Elm runner
let polyfills = std::fs::read_to_string(&elm_test_rs_root.join("templates/node_polyfills.js"))
.expect("polyfills.js template missing");
Expand Down Expand Up @@ -325,3 +346,74 @@ fn create_templated<P: AsRef<Path>>(template: P, output: P, replacements: Vec<(S
.write_all(content.as_bytes())
.expect("Unable to write to generated file");
}

fn add_kernel_test_checking(elm_js: &str) -> String {
lazy_static::lazy_static! {

/// For older versions of elm-explorations/test we need to list every single
/// variant of the `Test` type. To avoid having to update this regex if a new
/// variant is added, newer versions of elm-explorations/test have prefixed all
/// variants with `ElmTestVariant__` so we can match just on that.
/// TODO(harry): ask Lydell if the \s*\$:\s*(['"])\1\2 bit is important.
/// I had to remove this from the end because the regex crate does not
/// support them.
static ref TEST_VARIANT_DEFINITION: Regex = Regex::new(r#"(?m)^var\s+\$elm_explorations\$test\$Test\$Internal\$(ElmTestVariant__\w+|UnitTest|FuzzTest|Labeled|Skipped|Only|Batch)\s*=\s*(?:\w+\(\s*)?function\s*\([\w, ]*\)\s*\{\s*return\s*\{"#).unwrap();

static ref CHECK_DEFINITION: Regex = Regex::new(r#"(?m)^(var\s+\$author\$project\$Runner\$check)\s*=\s*\$author\$project\$Runner\$checkHelperReplaceMe___;?$"#).unwrap();
}

let elm_js =
TEST_VARIANT_DEFINITION.replace_all(&elm_js, "$0 __elmTestSymbol: __elmTestSymbol,");
let elm_js = CHECK_DEFINITION.replace(&elm_js, "$1 = value => value && value.__elmTestSymbol === __elmTestSymbol ? $$elm$$core$$Maybe$$Just(value) : $$elm$$core$$Maybe$$Nothing;");

["const __elmTestSymbol = Symbol('elmTestSymbol');", &elm_js].join("\n")
}

fn get_module_name(
source_dirs: impl IntoIterator<Item = impl AsRef<Path>> + Clone,
test_file: impl AsRef<Path>,
) -> String {
let matching_source_dir = {
let mut matching_dir_iter = source_dirs
.into_iter()
.filter(|dir| test_file.as_ref().starts_with(&dir));
if let Some(dir) = matching_dir_iter.next() {
let extra: Vec<_> = matching_dir_iter.collect();
if !extra.is_empty() {
panic!("2+ matching source dirs")
}
dir
} else {
panic!(
"This file:
{}
...matches no source directory! Imports won’t work then.
",
test_file.as_ref().display()
)
}
};

// By finding the module name from the file path we can import it even if
// the file is full of errors. Elm will then report what’s wrong.
let module_name_parts = dbg!(test_file.as_ref())
.strip_prefix(dbg!(&matching_source_dir.as_ref()))
.unwrap()
.components()
.map(|c| match c {
path::Component::Normal(s) => s.to_str().unwrap().replace(".elm", ""),
_ => panic!(),
})
.collect::<Vec<_>>();

assert!(module_name_parts.iter().all(|s| is_upper_name(s)));
assert!(!module_name_parts.is_empty());
module_name_parts.join(".")
}

fn is_upper_name(s: &str) -> bool {
lazy_static::lazy_static! {
static ref UPPER_NAME: Regex = Regex::new(r"^\p{Lu}[_\d\p{L}]*$").unwrap();
}
UPPER_NAME.is_match(s)
}
3 changes: 1 addition & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use std::path::{Path, PathBuf};
/// Find the root of the elm project (of current dir).
pub fn elm_project_root() -> Result<PathBuf, Box<dyn Error>> {
let current_dir = std::env::current_dir()?;
parent_traversal("elm.json", &current_dir)
.or_else(|_| Err("I didn't find any elm.json, are you in an Elm project".into()))
parent_traversal("elm.json", &current_dir).map_err(|_| "I didn't find any elm.json, are you in an Elm project".into())
}

/// Find where is located elm-test-rs on the system.
Expand Down
Loading

0 comments on commit ba6b277

Please sign in to comment.