Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically generate main file for unit tests #653

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.