Skip to content

Commit

Permalink
Merge pull request #653 from bartlomieju/auto_generate_test_file
Browse files Browse the repository at this point in the history
  • Loading branch information
yorickpeterse authored Nov 16, 2023
2 parents 8d3c6ef + da6ea1c commit 8f5ad1e
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 8f5ad1e

Please sign in to comment.