Skip to content

Commit

Permalink
Automatically generate main file for unit tests
Browse files Browse the repository at this point in the history
This commit implements automatic generation of the main file
(inko-tests.inko) for running unit tests. This file is placed
in the build/ directory and is regenerated on each run. The file
is not removed after the tests are run to allow inspection.

Changelog: added
  • Loading branch information
bartlomieju authored and yorickpeterse committed Nov 16, 2023
1 parent 1687c75 commit da6ea1c
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 143 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions compiler/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
Expand Down
13 changes: 10 additions & 3 deletions compiler/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
32 changes: 0 additions & 32 deletions docs/source/guides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ tests for the standard library are organised as follows:

```
std/test/
├── main.inko
└── std
├── fs
│ ├── test_dir.inko
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions inko/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ license = "MPL-2.0"
[dependencies]
getopts = "^0.2"
compiler = { path = "../compiler" }
types = { path = "../types" }
98 changes: 97 additions & 1 deletion inko/src/command/test.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -42,7 +45,20 @@ pub(crate) fn run(arguments: &[String]) -> Result<i32, Error> {
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();
Expand All @@ -60,3 +76,83 @@ pub(crate) fn run(arguments: &[String]) -> Result<i32, Error> {
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<Vec<PathBuf>, 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<Vec<ModuleName>, 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::<Vec<_>>();

Ok(test_modules)
}

fn generate_main_test_module(tests: Vec<ModuleName>) -> 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
}
107 changes: 0 additions & 107 deletions std/test/main.inko

This file was deleted.

0 comments on commit da6ea1c

Please sign in to comment.