diff --git a/Cargo.lock b/Cargo.lock index 81a294bb6..d6f3e8d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,7 @@ version = "0.13.1" dependencies = [ "compiler", "getopts", + "types", ] [[package]] diff --git a/compiler/src/compiler.rs b/compiler/src/compiler.rs index 4e10f8100..bff6b42b0 100644 --- a/compiler/src/compiler.rs +++ b/compiler/src/compiler.rs @@ -93,6 +93,10 @@ impl Compiler { self.state.config.presenter.present(&self.state.diagnostics); } + pub fn create_build_directory(&self) -> Result<(), String> { + BuildDirectories::new(&self.state.config).create_build() + } + fn main_module_path( &mut self, file: Option, diff --git a/compiler/src/config.rs b/compiler/src/config.rs index 1f3c569c8..eda3f12ca 100644 --- a/compiler/src/config.rs +++ b/compiler/src/config.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use types::module_name::ModuleName; /// The extension to use for source files. -pub(crate) const SOURCE_EXT: &str = "inko"; +pub const SOURCE_EXT: &str = "inko"; /// The name of the module to compile if no explicit file/module is provided. pub(crate) const MAIN_MODULE: &str = "main"; @@ -21,6 +21,9 @@ pub const DEP: &str = "dep"; /// The name of the directory containing a project's unit tests. pub(crate) const TESTS: &str = "test"; +/// The name of the module that runs tests. +const MAIN_TEST_MODULE: &str = "inko-tests"; + /// The name of the directory to store build files in. const BUILD: &str = "build"; @@ -67,11 +70,15 @@ impl BuildDirectories { } pub(crate) fn create(&self) -> Result<(), String> { - create_directory(&self.build) + self.create_build() .and_then(|_| create_directory(&self.objects)) .and_then(|_| create_directory(&self.bin)) } + pub(crate) fn create_build(&self) -> Result<(), String> { + create_directory(&self.build) + } + pub(crate) fn create_dot(&self) -> Result<(), String> { create_directory(&self.dot) } @@ -256,7 +263,7 @@ impl Config { } pub fn main_test_module(&self) -> PathBuf { - let mut main_file = self.tests.join(MAIN_MODULE); + let mut main_file = self.build.join(MAIN_TEST_MODULE); main_file.set_extension(SOURCE_EXT); main_file diff --git a/docs/source/guides/testing.md b/docs/source/guides/testing.md index 670939f9a..a6bf0caf9 100644 --- a/docs/source/guides/testing.md +++ b/docs/source/guides/testing.md @@ -39,7 +39,6 @@ tests for the standard library are organised as follows: ``` std/test/ -├── main.inko └── std ├── fs │ ├── test_dir.inko @@ -54,37 +53,6 @@ std/test/ └── test_tuple.inko ``` -In a test directory you should create a `main.inko` file. This file imports and -registers all your tests, and is run when using the `inko test` command. Here's -what such a file might look like: - -```inko -import std.env -import std.test.(Filter, Tests) - -import std.test_array -import std.test_bool -import std.test_byte_array -import std.test_tuple - -class async Main { - fn async main { - let tests = Tests.new - - test_array.tests(tests) - test_bool.tests(tests) - test_byte_array.tests(tests) - test_tuple.tests(tests) - - tests.filter = Filter.from_string(env.arguments.opt(0).unwrap_or('')) - tests.run - } -} -``` - -In the future Inko may generate this file for you, but for the time being it -needs to be maintained manually. - ## Running tests With these files in place you can run your tests using `inko test`. When doing diff --git a/inko/Cargo.toml b/inko/Cargo.toml index 605879274..ec7c0f163 100644 --- a/inko/Cargo.toml +++ b/inko/Cargo.toml @@ -8,3 +8,4 @@ license = "MPL-2.0" [dependencies] getopts = "^0.2" compiler = { path = "../compiler" } +types = { path = "../types" } diff --git a/inko/src/command/test.rs b/inko/src/command/test.rs index 1b7e5a7b6..5fa0df5ca 100644 --- a/inko/src/command/test.rs +++ b/inko/src/command/test.rs @@ -1,9 +1,12 @@ use crate::error::Error; use crate::options::print_usage; use compiler::compiler::{CompileError, Compiler}; -use compiler::config::{Config, Output}; +use compiler::config::{Config, Output, SOURCE_EXT}; use getopts::Options; +use std::fs::{read_dir, write}; +use std::path::{Path, PathBuf}; use std::process::Command; +use types::module_name::ModuleName; const USAGE: &str = "Usage: inko test [OPTIONS] @@ -42,7 +45,20 @@ pub(crate) fn run(arguments: &[String]) -> Result { config.add_source_directory(config.tests.clone()); config.output = Output::File("inko-tests".to_string()); + let tests = test_module_names(&config.tests).map_err(|err| { + Error::generic(format!("Failed to find test modules: {}", err)) + })?; + let mut compiler = Compiler::new(config); + + // The build/ directory needs to be created first, otherwise we can't save + // the generated file in it (if it doesn't already exist that is). + compiler.create_build_directory()?; + + write(&input, generate_main_test_module(tests)).map_err(|err| { + Error::generic(format!("Failed to write {}: {}", input.display(), err)) + })?; + let result = compiler.build(Some(input)); compiler.print_diagnostics(); @@ -60,3 +76,83 @@ pub(crate) fn run(arguments: &[String]) -> Result { Err(CompileError::Internal(msg)) => Err(Error::generic(msg)), } } + +fn is_test_file(path: &Path) -> bool { + match path.extension().and_then(|p| p.to_str()) { + Some(SOURCE_EXT) if path.is_file() => {} + _ => return false, + } + + path.file_name() + .map(|v| v.to_string_lossy()) + .map_or(false, |v| v.starts_with("test_")) +} + +fn test_files(test_dir: &Path) -> Result, std::io::Error> { + let mut found = Vec::new(); + let mut pending = vec![test_dir.to_owned()]; + + while let Some(path) = pending.pop() { + let entries = read_dir(&path)?; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + pending.push(path); + continue; + } + + if is_test_file(&path) { + found.push(path); + } + } + } + + Ok(found) +} + +fn test_module_names( + test_dir: &Path, +) -> Result, std::io::Error> { + let test_modules = test_files(test_dir)? + .into_iter() + .map(|file| { + ModuleName::from_relative_path(file.strip_prefix(test_dir).unwrap()) + }) + .collect::>(); + + Ok(test_modules) +} + +fn generate_main_test_module(tests: Vec) -> String { + let mut imports = Vec::with_capacity(tests.len()); + let mut calls = Vec::with_capacity(tests.len()); + + for (idx, test) in tests.iter().enumerate() { + imports.push(format!("import {}.(self as tests{})\n", test, idx)); + calls.push(format!(" tests{}.tests(tests)\n", idx)); + } + + let mut source = + "import std.env\nimport std.test.(Filter, Tests)\n".to_string(); + + for line in imports { + source.push_str(&line); + } + + source.push_str("\nclass async Main {\n"); + source.push_str(" fn async main {\n"); + source.push_str(" let tests = Tests.new\n\n"); + + for line in calls { + source.push_str(&line); + } + + source.push_str(" tests.filter = Filter.from_string(env.arguments.opt(0).unwrap_or(''))\n"); + source.push_str(" tests.run\n"); + source.push_str(" }\n"); + source.push_str("}\n"); + source +} diff --git a/std/test/main.inko b/std/test/main.inko deleted file mode 100644 index 0cce58db9..000000000 --- a/std/test/main.inko +++ /dev/null @@ -1,107 +0,0 @@ -import std.env -import std.test.(Filter, Tests) - -import compiler.test_casts -import compiler.test_constants -import compiler.test_drop -import compiler.test_pattern_matching -import std.crypto.test_chacha -import std.crypto.test_hash -import std.crypto.test_math -import std.crypto.test_md5 -import std.crypto.test_poly1305 -import std.crypto.test_sha1 -import std.crypto.test_sha2 -import std.endian.test_big -import std.endian.test_little -import std.fs.test_file -import std.fs.test_path -import std.hash.test_siphash -import std.net.test_ip -import std.net.test_socket -import std.test_array -import std.test_bool -import std.test_byte_array -import std.test_channel -import std.test_cmp -import std.test_debug -import std.test_env -import std.test_float -import std.test_fmt -import std.test_fs -import std.test_int -import std.test_io -import std.test_iter -import std.test_json -import std.test_map -import std.test_nil -import std.test_option -import std.test_process -import std.test_rand -import std.test_range -import std.test_result -import std.test_set -import std.test_stdio -import std.test_string -import std.test_sys -import std.test_test -import std.test_time -import std.test_tuple -import std.test_utf8 - -class async Main { - fn async main { - let tests = Tests.new - - test_array.tests(tests) - test_big.tests(tests) - test_bool.tests(tests) - test_byte_array.tests(tests) - test_casts.tests(tests) - test_chacha.tests(tests) - test_channel.tests(tests) - test_cmp.tests(tests) - test_constants.tests(tests) - test_debug.tests(tests) - test_drop.tests(tests) - test_env.tests(tests) - test_file.tests(tests) - test_float.tests(tests) - test_fmt.tests(tests) - test_fs.tests(tests) - test_hash.tests(tests) - test_int.tests(tests) - test_io.tests(tests) - test_ip.tests(tests) - test_iter.tests(tests) - test_json.tests(tests) - test_little.tests(tests) - test_map.tests(tests) - test_math.tests(tests) - test_md5.tests(tests) - test_nil.tests(tests) - test_option.tests(tests) - test_path.tests(tests) - test_pattern_matching.tests(tests) - test_poly1305.tests(tests) - test_process.tests(tests) - test_rand.tests(tests) - test_range.tests(tests) - test_result.tests(tests) - test_set.tests(tests) - test_sha1.tests(tests) - test_sha2.tests(tests) - test_siphash.tests(tests) - test_socket.tests(tests) - test_stdio.tests(tests) - test_string.tests(tests) - test_sys.tests(tests) - test_test.tests(tests) - test_time.tests(tests) - test_tuple.tests(tests) - test_utf8.tests(tests) - - tests.filter = Filter.from_string(env.arguments.opt(0).unwrap_or('')) - tests.run - } -}