Merge pull request #160 from txpipe/pairing_testing
Aiken Testing Framework
This commit is contained in:
commit
0449c818e5
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -2,6 +2,30 @@
|
||||||
|
|
||||||
## [next] - 2022-MM-DD
|
## [next] - 2022-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **aiken-lang**: integrated unit tests
|
||||||
|
|
||||||
|
Aiken now supports writing unit tests directly in source files using the new
|
||||||
|
`test` keyword. Tests are functions with no arguments that are implicitly typed
|
||||||
|
to `bool`. For example:
|
||||||
|
|
||||||
|
```gleam
|
||||||
|
test foo () {
|
||||||
|
1 + 1 == 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **aiken**: new `--skip-tests` flag for the `check` command
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **aiken**: `check` now also runs and reports on any `test` found in the project
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
N/A
|
||||||
|
|
||||||
## [v0.0.26] - 2022-11-23
|
## [v0.0.26] - 2022-11-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -59,6 +59,7 @@ dependencies = [
|
||||||
"ignore",
|
"ignore",
|
||||||
"indoc",
|
"indoc",
|
||||||
"miette",
|
"miette",
|
||||||
|
"owo-colors",
|
||||||
"pallas-addresses",
|
"pallas-addresses",
|
||||||
"pallas-codec",
|
"pallas-codec",
|
||||||
"pallas-crypto",
|
"pallas-crypto",
|
||||||
|
|
|
@ -15,6 +15,7 @@ hex = "0.4.3"
|
||||||
ignore = "0.4.18"
|
ignore = "0.4.18"
|
||||||
indoc = "1.0"
|
indoc = "1.0"
|
||||||
miette = { version = "5.3.0", features = ["fancy"] }
|
miette = { version = "5.3.0", features = ["fancy"] }
|
||||||
|
owo-colors = "3.5.0"
|
||||||
pallas-addresses = "0.14.0"
|
pallas-addresses = "0.14.0"
|
||||||
pallas-codec = "0.14.0"
|
pallas-codec = "0.14.0"
|
||||||
pallas-crypto = "0.14.0"
|
pallas-crypto = "0.14.0"
|
||||||
|
|
|
@ -6,8 +6,17 @@ pub struct Args {
|
||||||
/// Path to project
|
/// Path to project
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
directory: Option<PathBuf>,
|
directory: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Skip tests; run only the type-checker
|
||||||
|
#[clap(short, long)]
|
||||||
|
skip_tests: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(Args { directory }: Args) -> miette::Result<()> {
|
pub fn exec(
|
||||||
crate::with_project(directory, |p| p.check())
|
Args {
|
||||||
|
directory,
|
||||||
|
skip_tests,
|
||||||
|
}: Args,
|
||||||
|
) -> miette::Result<()> {
|
||||||
|
crate::with_project(directory, |p| p.check(skip_tests))
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,9 @@ pub fn exec(
|
||||||
program = program.apply_term(&term);
|
program = program.apply_term(&term);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (term, cost, logs) = program.eval();
|
let budget = ExBudget::default();
|
||||||
|
|
||||||
|
let (term, cost, logs) = program.eval(budget);
|
||||||
|
|
||||||
match term {
|
match term {
|
||||||
Ok(term) => {
|
Ok(term) => {
|
||||||
|
@ -74,8 +76,6 @@ pub fn exec(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let budget = ExBudget::default();
|
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\nCosts\n-----\ncpu: {}\nmemory: {}",
|
"\nCosts\n-----\ncpu: {}\nmemory: {}",
|
||||||
budget.cpu - cost.cpu,
|
budget.cpu - cost.cpu,
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
pub mod cmd;
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
use aiken_project::{config::Config, Project};
|
use aiken_project::{
|
||||||
|
config::Config,
|
||||||
|
telemetry::{self, TestInfo},
|
||||||
|
Project,
|
||||||
|
};
|
||||||
use miette::IntoDiagnostic;
|
use miette::IntoDiagnostic;
|
||||||
use std::env;
|
use owo_colors::OwoColorize;
|
||||||
use std::path::PathBuf;
|
use uplc::machine::cost_model::ExBudget;
|
||||||
|
|
||||||
|
pub mod cmd;
|
||||||
|
|
||||||
pub fn with_project<A>(directory: Option<PathBuf>, mut action: A) -> miette::Result<()>
|
pub fn with_project<A>(directory: Option<PathBuf>, mut action: A) -> miette::Result<()>
|
||||||
where
|
where
|
||||||
A: FnMut(&mut Project) -> Result<(), aiken_project::error::Error>,
|
A: FnMut(&mut Project<Terminal>) -> Result<(), aiken_project::error::Error>,
|
||||||
{
|
{
|
||||||
let project_path = if let Some(d) = directory {
|
let project_path = if let Some(d) = directory {
|
||||||
d
|
d
|
||||||
|
@ -17,7 +23,7 @@ where
|
||||||
|
|
||||||
let config = Config::load(project_path.clone()).into_diagnostic()?;
|
let config = Config::load(project_path.clone()).into_diagnostic()?;
|
||||||
|
|
||||||
let mut project = Project::new(config, project_path);
|
let mut project = Project::new(config, project_path, Terminal::default());
|
||||||
|
|
||||||
let build_result = action(&mut project);
|
let build_result = action(&mut project);
|
||||||
|
|
||||||
|
@ -30,10 +36,129 @@ where
|
||||||
if let Err(err) = build_result {
|
if let Err(err) = build_result {
|
||||||
err.report();
|
err.report();
|
||||||
|
|
||||||
miette::bail!("failed: {} error(s), {warning_count} warning(s)", err.len(),);
|
miette::bail!("Failed: {} error(s), {warning_count} warning(s)", err.len(),);
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("\nfinished with {warning_count} warning(s)\n");
|
println!("\nFinished with {warning_count} warning(s)\n");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub struct Terminal;
|
||||||
|
|
||||||
|
impl telemetry::EventListener for Terminal {
|
||||||
|
fn handle_event(&self, event: telemetry::Event) {
|
||||||
|
match event {
|
||||||
|
telemetry::Event::StartingCompilation {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
root,
|
||||||
|
} => {
|
||||||
|
println!(
|
||||||
|
"{} {} {} ({})",
|
||||||
|
"Compiling".bold().purple(),
|
||||||
|
name.bold(),
|
||||||
|
version,
|
||||||
|
root.to_str().unwrap_or("").bright_blue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
telemetry::Event::ParsingProjectFiles => {
|
||||||
|
println!("{}", "...Parsing project files".bold().purple());
|
||||||
|
}
|
||||||
|
telemetry::Event::TypeChecking => {
|
||||||
|
println!("{}", "...Type-checking project".bold().purple());
|
||||||
|
}
|
||||||
|
telemetry::Event::GeneratingUPLC { output_path } => {
|
||||||
|
println!(
|
||||||
|
"{} in {}",
|
||||||
|
"...Generating Untyped Plutus Core".bold().purple(),
|
||||||
|
output_path.to_str().unwrap_or("").bright_blue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
telemetry::Event::RunningTests => {
|
||||||
|
println!("{}\n", "...Running tests".bold().purple());
|
||||||
|
}
|
||||||
|
telemetry::Event::FinishedTests { tests } => {
|
||||||
|
let (max_mem, max_cpu) = tests.iter().fold(
|
||||||
|
(0, 0),
|
||||||
|
|(max_mem, max_cpu), TestInfo { spent_budget, .. }| {
|
||||||
|
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
|
||||||
|
(spent_budget.mem, spent_budget.cpu)
|
||||||
|
} else if spent_budget.mem > max_mem {
|
||||||
|
(spent_budget.mem, max_cpu)
|
||||||
|
} else if spent_budget.cpu > max_cpu {
|
||||||
|
(max_mem, spent_budget.cpu)
|
||||||
|
} else {
|
||||||
|
(max_mem, max_cpu)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let max_mem = max_mem.to_string().len() as i32;
|
||||||
|
let max_cpu = max_cpu.to_string().len() as i32;
|
||||||
|
|
||||||
|
for test_info in &tests {
|
||||||
|
println!("{}", fmt_test(test_info, max_mem, max_cpu))
|
||||||
|
}
|
||||||
|
|
||||||
|
let (n_passed, n_failed) =
|
||||||
|
tests
|
||||||
|
.iter()
|
||||||
|
.fold((0, 0), |(n_passed, n_failed), test_info| {
|
||||||
|
if test_info.is_passing {
|
||||||
|
(n_passed + 1, n_failed)
|
||||||
|
} else {
|
||||||
|
(n_passed, n_failed + 1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"\n Summary: {} test(s), {}; {}.",
|
||||||
|
tests.len(),
|
||||||
|
format!("{} passed", n_passed).bright_green(),
|
||||||
|
format!("{} failed", n_failed).bright_red()
|
||||||
|
)
|
||||||
|
.bold()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_test(test_info: &TestInfo, max_mem: i32, max_cpu: i32) -> String {
|
||||||
|
let TestInfo {
|
||||||
|
is_passing,
|
||||||
|
test,
|
||||||
|
spent_budget,
|
||||||
|
} = test_info;
|
||||||
|
|
||||||
|
let ExBudget { mem, cpu } = spent_budget;
|
||||||
|
|
||||||
|
format!(
|
||||||
|
" [{}] [mem: {}, cpu: {}] {}::{}",
|
||||||
|
if *is_passing {
|
||||||
|
"PASS".bold().green().to_string()
|
||||||
|
} else {
|
||||||
|
"FAIL".bold().red().to_string()
|
||||||
|
},
|
||||||
|
pad_left(mem.to_string(), max_mem, " "),
|
||||||
|
pad_left(cpu.to_string(), max_cpu, " "),
|
||||||
|
test.module.blue(),
|
||||||
|
test.name.bright_blue()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_left(mut text: String, n: i32, delimiter: &str) -> String {
|
||||||
|
let diff = n - text.len() as i32;
|
||||||
|
|
||||||
|
if diff.is_positive() {
|
||||||
|
for _ in 0..diff {
|
||||||
|
text.insert_str(0, delimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
|
@ -138,6 +138,8 @@ pub enum Definition<T, Expr, ConstantRecordTag, PackageName> {
|
||||||
Use(Use<PackageName>),
|
Use(Use<PackageName>),
|
||||||
|
|
||||||
ModuleConstant(ModuleConstant<T, ConstantRecordTag>),
|
ModuleConstant(ModuleConstant<T, ConstantRecordTag>),
|
||||||
|
|
||||||
|
Test(Function<T, Expr>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A, B, C, E> Definition<A, B, C, E> {
|
impl<A, B, C, E> Definition<A, B, C, E> {
|
||||||
|
@ -147,7 +149,8 @@ impl<A, B, C, E> Definition<A, B, C, E> {
|
||||||
| Definition::Use(Use { location, .. })
|
| Definition::Use(Use { location, .. })
|
||||||
| Definition::TypeAlias(TypeAlias { location, .. })
|
| Definition::TypeAlias(TypeAlias { location, .. })
|
||||||
| Definition::DataType(DataType { location, .. })
|
| Definition::DataType(DataType { location, .. })
|
||||||
| Definition::ModuleConstant(ModuleConstant { location, .. }) => *location,
|
| Definition::ModuleConstant(ModuleConstant { location, .. })
|
||||||
|
| Definition::Test(Function { location, .. }) => *location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +160,8 @@ impl<A, B, C, E> Definition<A, B, C, E> {
|
||||||
Definition::Fn(Function { doc, .. })
|
Definition::Fn(Function { doc, .. })
|
||||||
| Definition::TypeAlias(TypeAlias { doc, .. })
|
| Definition::TypeAlias(TypeAlias { doc, .. })
|
||||||
| Definition::DataType(DataType { doc, .. })
|
| Definition::DataType(DataType { doc, .. })
|
||||||
| Definition::ModuleConstant(ModuleConstant { doc, .. }) => {
|
| Definition::ModuleConstant(ModuleConstant { doc, .. })
|
||||||
|
| Definition::Test(Function { doc, .. }) => {
|
||||||
let _ = std::mem::replace(doc, Some(new_doc));
|
let _ = std::mem::replace(doc, Some(new_doc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,23 @@ impl<'comments> Formatter<'comments> {
|
||||||
return_annotation,
|
return_annotation,
|
||||||
end_position,
|
end_position,
|
||||||
..
|
..
|
||||||
}) => self.definition_fn(public, name, args, return_annotation, body, *end_position),
|
}) => self.definition_fn(
|
||||||
|
public,
|
||||||
|
"fn",
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
return_annotation,
|
||||||
|
body,
|
||||||
|
*end_position,
|
||||||
|
),
|
||||||
|
|
||||||
|
Definition::Test(Function {
|
||||||
|
name,
|
||||||
|
arguments: args,
|
||||||
|
body,
|
||||||
|
end_position,
|
||||||
|
..
|
||||||
|
}) => self.definition_fn(&false, "test", name, args, &None, body, *end_position),
|
||||||
|
|
||||||
Definition::TypeAlias(TypeAlias {
|
Definition::TypeAlias(TypeAlias {
|
||||||
alias,
|
alias,
|
||||||
|
@ -493,9 +509,11 @@ impl<'comments> Formatter<'comments> {
|
||||||
commented(doc, comments)
|
commented(doc, comments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn definition_fn<'a>(
|
fn definition_fn<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
public: &'a bool,
|
public: &'a bool,
|
||||||
|
keyword: &'a str,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
args: &'a [UntypedArg],
|
args: &'a [UntypedArg],
|
||||||
return_annotation: &'a Option<Annotation>,
|
return_annotation: &'a Option<Annotation>,
|
||||||
|
@ -504,7 +522,8 @@ impl<'comments> Formatter<'comments> {
|
||||||
) -> Document<'a> {
|
) -> Document<'a> {
|
||||||
// Fn name and args
|
// Fn name and args
|
||||||
let head = pub_(*public)
|
let head = pub_(*public)
|
||||||
.append("fn ")
|
.append(keyword)
|
||||||
|
.append(" ")
|
||||||
.append(name)
|
.append(name)
|
||||||
.append(wrap_args(args.iter().map(|e| (self.fn_arg(e), false))));
|
.append(wrap_args(args.iter().map(|e| (self.fn_arg(e), false))));
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ fn module_parser() -> impl Parser<Token, Vec<UntypedDefinition>, Error = ParseEr
|
||||||
data_parser(),
|
data_parser(),
|
||||||
type_alias_parser(),
|
type_alias_parser(),
|
||||||
fn_parser(),
|
fn_parser(),
|
||||||
|
test_parser(),
|
||||||
constant_parser(),
|
constant_parser(),
|
||||||
))
|
))
|
||||||
.repeated()
|
.repeated()
|
||||||
|
@ -266,6 +267,36 @@ pub fn fn_parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseEr
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn test_parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError> {
|
||||||
|
just(Token::Test)
|
||||||
|
.ignore_then(select! {Token::Name {name} => name})
|
||||||
|
.then_ignore(just(Token::LeftParen))
|
||||||
|
.then_ignore(just(Token::RightParen))
|
||||||
|
.map_with_span(|name, span| (name, span))
|
||||||
|
.then(
|
||||||
|
expr_seq_parser()
|
||||||
|
.or_not()
|
||||||
|
.delimited_by(just(Token::LeftBrace), just(Token::RightBrace)),
|
||||||
|
)
|
||||||
|
.map_with_span(|((name, span_end), body), span| {
|
||||||
|
ast::UntypedDefinition::Test(ast::Function {
|
||||||
|
arguments: vec![],
|
||||||
|
body: body.unwrap_or(expr::UntypedExpr::Todo {
|
||||||
|
kind: TodoKind::EmptyFunction,
|
||||||
|
location: span,
|
||||||
|
label: None,
|
||||||
|
}),
|
||||||
|
doc: None,
|
||||||
|
location: span_end,
|
||||||
|
end_position: span.end - 1,
|
||||||
|
name,
|
||||||
|
public: true,
|
||||||
|
return_annotation: None,
|
||||||
|
return_type: (),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn constant_parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError> {
|
fn constant_parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError> {
|
||||||
pub_parser()
|
pub_parser()
|
||||||
.or_not()
|
.or_not()
|
||||||
|
|
|
@ -67,6 +67,7 @@ pub fn lexer() -> impl Parser<char, Vec<(Token, Span)>, Error = ParseError> {
|
||||||
"check" => Token::Assert,
|
"check" => Token::Assert,
|
||||||
"const" => Token::Const,
|
"const" => Token::Const,
|
||||||
"fn" => Token::Fn,
|
"fn" => Token::Fn,
|
||||||
|
"test" => Token::Test,
|
||||||
"if" => Token::If,
|
"if" => Token::If,
|
||||||
"else" => Token::Else,
|
"else" => Token::Else,
|
||||||
"is" => Token::Is,
|
"is" => Token::Is,
|
||||||
|
|
|
@ -68,6 +68,7 @@ pub enum Token {
|
||||||
Opaque,
|
Opaque,
|
||||||
Pub,
|
Pub,
|
||||||
Use,
|
Use,
|
||||||
|
Test,
|
||||||
Todo,
|
Todo,
|
||||||
Trace,
|
Trace,
|
||||||
Type,
|
Type,
|
||||||
|
@ -145,6 +146,7 @@ impl fmt::Display for Token {
|
||||||
Token::Todo => "todo",
|
Token::Todo => "todo",
|
||||||
Token::Trace => "try",
|
Token::Trace => "try",
|
||||||
Token::Type => "type",
|
Token::Type => "type",
|
||||||
|
Token::Test => "test",
|
||||||
};
|
};
|
||||||
write!(f, "\"{}\"", s)
|
write!(f, "\"{}\"", s)
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,6 +255,7 @@ impl<'a> Environment<'a> {
|
||||||
definition @ (Definition::TypeAlias { .. }
|
definition @ (Definition::TypeAlias { .. }
|
||||||
| Definition::DataType { .. }
|
| Definition::DataType { .. }
|
||||||
| Definition::Use { .. }
|
| Definition::Use { .. }
|
||||||
|
| Definition::Test { .. }
|
||||||
| Definition::ModuleConstant { .. }) => definition,
|
| Definition::ModuleConstant { .. }) => definition,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -911,7 +912,10 @@ impl<'a> Environment<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Definition::Fn { .. } | Definition::Use { .. } | Definition::ModuleConstant { .. } => {}
|
Definition::Fn { .. }
|
||||||
|
| Definition::Test { .. }
|
||||||
|
| Definition::Use { .. }
|
||||||
|
| Definition::ModuleConstant { .. } => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -990,6 +994,24 @@ impl<'a> Environment<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Definition::Test(Function { name, location, .. }) => {
|
||||||
|
hydrators.insert(name.clone(), Hydrator::new());
|
||||||
|
let arg_types = vec![];
|
||||||
|
let return_type = builtins::bool();
|
||||||
|
self.insert_variable(
|
||||||
|
name.clone(),
|
||||||
|
ValueConstructorVariant::ModuleFn {
|
||||||
|
name: name.clone(),
|
||||||
|
field_map: None,
|
||||||
|
module: module_name.to_owned(),
|
||||||
|
arity: 0,
|
||||||
|
location: *location,
|
||||||
|
builtin: None,
|
||||||
|
},
|
||||||
|
function(arg_types, return_type),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Definition::DataType(DataType {
|
Definition::DataType(DataType {
|
||||||
location,
|
location,
|
||||||
public,
|
public,
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
||||||
RecordConstructorArg, TypeAlias, TypedDefinition, TypedModule, UntypedDefinition,
|
RecordConstructorArg, TypeAlias, TypedDefinition, TypedModule, UntypedDefinition,
|
||||||
UntypedModule, Use,
|
UntypedModule, Use,
|
||||||
},
|
},
|
||||||
|
builtins,
|
||||||
builtins::function,
|
builtins::function,
|
||||||
parser::token::Token,
|
parser::token::Token,
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
|
@ -66,8 +67,8 @@ impl UntypedModule {
|
||||||
for def in self.definitions().cloned() {
|
for def in self.definitions().cloned() {
|
||||||
match def {
|
match def {
|
||||||
Definition::ModuleConstant { .. } => consts.push(def),
|
Definition::ModuleConstant { .. } => consts.push(def),
|
||||||
|
|
||||||
Definition::Fn { .. }
|
Definition::Fn { .. }
|
||||||
|
| Definition::Test { .. }
|
||||||
| Definition::TypeAlias { .. }
|
| Definition::TypeAlias { .. }
|
||||||
| Definition::DataType { .. }
|
| Definition::DataType { .. }
|
||||||
| Definition::Use { .. } => not_consts.push(def),
|
| Definition::Use { .. } => not_consts.push(def),
|
||||||
|
@ -233,6 +234,17 @@ fn infer_definition(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Definition::Test(f) => {
|
||||||
|
if let Definition::Fn(f) =
|
||||||
|
infer_definition(Definition::Fn(f), module_name, hydrators, environment)?
|
||||||
|
{
|
||||||
|
environment.unify(f.return_type.clone(), builtins::bool(), f.location)?;
|
||||||
|
Ok(Definition::Test(f))
|
||||||
|
} else {
|
||||||
|
unreachable!("test defintion inferred as something else than a function?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Definition::TypeAlias(TypeAlias {
|
Definition::TypeAlias(TypeAlias {
|
||||||
doc,
|
doc,
|
||||||
location,
|
location,
|
||||||
|
|
|
@ -293,7 +293,7 @@ impl Diagnostic for Warning {
|
||||||
|
|
||||||
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||||
match self {
|
match self {
|
||||||
Warning::Type { .. } => Some(Box::new("aiken::typecheck")),
|
Warning::Type { .. } => Some(Box::new("aiken::check")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub mod error;
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod script;
|
pub mod script;
|
||||||
|
pub mod telemetry;
|
||||||
|
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{Definition, Function, ModuleKind, TypedFunction},
|
ast::{Definition, Function, ModuleKind, TypedFunction},
|
||||||
|
@ -25,12 +26,17 @@ use pallas::{
|
||||||
use pallas_traverse::ComputeHash;
|
use pallas_traverse::ComputeHash;
|
||||||
use script::Script;
|
use script::Script;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use uplc::ast::{DeBruijn, Program};
|
use telemetry::{EventListener, TestInfo};
|
||||||
|
use uplc::{
|
||||||
|
ast::{DeBruijn, Program},
|
||||||
|
machine::cost_model::ExBudget,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
error::{Error, Warning},
|
error::{Error, Warning},
|
||||||
module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules},
|
module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules},
|
||||||
|
telemetry::Event,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -47,7 +53,10 @@ pub const MINT: &str = "mint";
|
||||||
pub const WITHDRAWL: &str = "withdrawl";
|
pub const WITHDRAWL: &str = "withdrawl";
|
||||||
pub const VALIDATOR_NAMES: [&str; 4] = [SPEND, CERT, MINT, WITHDRAWL];
|
pub const VALIDATOR_NAMES: [&str; 4] = [SPEND, CERT, MINT, WITHDRAWL];
|
||||||
|
|
||||||
pub struct Project {
|
pub struct Project<T>
|
||||||
|
where
|
||||||
|
T: EventListener,
|
||||||
|
{
|
||||||
config: Config,
|
config: Config,
|
||||||
defined_modules: HashMap<String, PathBuf>,
|
defined_modules: HashMap<String, PathBuf>,
|
||||||
id_gen: IdGenerator,
|
id_gen: IdGenerator,
|
||||||
|
@ -55,10 +64,14 @@ pub struct Project {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
sources: Vec<Source>,
|
sources: Vec<Source>,
|
||||||
pub warnings: Vec<Warning>,
|
pub warnings: Vec<Warning>,
|
||||||
|
event_listener: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl<T> Project<T>
|
||||||
pub fn new(config: Config, root: PathBuf) -> Project {
|
where
|
||||||
|
T: EventListener,
|
||||||
|
{
|
||||||
|
pub fn new(config: Config, root: PathBuf, event_listener: T) -> Project<T> {
|
||||||
let id_gen = IdGenerator::new();
|
let id_gen = IdGenerator::new();
|
||||||
|
|
||||||
let mut module_types = HashMap::new();
|
let mut module_types = HashMap::new();
|
||||||
|
@ -74,34 +87,60 @@ impl Project {
|
||||||
root,
|
root,
|
||||||
sources: vec![],
|
sources: vec![],
|
||||||
warnings: vec![],
|
warnings: vec![],
|
||||||
|
event_listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&mut self, uplc: bool) -> Result<(), Error> {
|
pub fn build(&mut self, uplc: bool) -> Result<(), Error> {
|
||||||
self.compile(true, uplc)
|
self.compile(true, uplc, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(&mut self) -> Result<(), Error> {
|
pub fn check(&mut self, skip_tests: bool) -> Result<(), Error> {
|
||||||
self.compile(false, false)
|
self.compile(false, false, !skip_tests)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(&mut self, uplc_gen: bool, uplc_dump: bool) -> Result<(), Error> {
|
pub fn compile(
|
||||||
|
&mut self,
|
||||||
|
uplc_gen: bool,
|
||||||
|
uplc_dump: bool,
|
||||||
|
run_tests: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.event_listener
|
||||||
|
.handle_event(Event::StartingCompilation {
|
||||||
|
root: self.root.clone(),
|
||||||
|
name: self.config.name.clone(),
|
||||||
|
version: self.config.version.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
self.event_listener.handle_event(Event::ParsingProjectFiles);
|
||||||
|
|
||||||
self.read_source_files()?;
|
self.read_source_files()?;
|
||||||
|
|
||||||
let parsed_modules = self.parse_sources()?;
|
let parsed_modules = self.parse_sources()?;
|
||||||
|
|
||||||
let processing_sequence = parsed_modules.sequence()?;
|
let processing_sequence = parsed_modules.sequence()?;
|
||||||
|
|
||||||
|
self.event_listener.handle_event(Event::TypeChecking);
|
||||||
|
|
||||||
let mut checked_modules = self.type_check(parsed_modules, processing_sequence)?;
|
let mut checked_modules = self.type_check(parsed_modules, processing_sequence)?;
|
||||||
|
|
||||||
let validators = self.validate_validators(&mut checked_modules)?;
|
let validators = self.validate_validators(&mut checked_modules)?;
|
||||||
|
|
||||||
|
// TODO: In principle, uplc_gen and run_tests can't be true together. We probably want to
|
||||||
|
// model the options differently to make it obvious at the type-level.
|
||||||
if uplc_gen {
|
if uplc_gen {
|
||||||
|
self.event_listener.handle_event(Event::GeneratingUPLC {
|
||||||
|
output_path: self.output_path(),
|
||||||
|
});
|
||||||
let programs = self.code_gen(validators, &checked_modules)?;
|
let programs = self.code_gen(validators, &checked_modules)?;
|
||||||
|
|
||||||
self.write_build_outputs(programs, uplc_dump)?;
|
self.write_build_outputs(programs, uplc_dump)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if run_tests {
|
||||||
|
let tests = self.test_gen(&checked_modules)?;
|
||||||
|
self.run_tests(tests);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,6 +375,7 @@ impl Project {
|
||||||
func,
|
func,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Definition::Test(_) => {}
|
||||||
Definition::TypeAlias(ta) => {
|
Definition::TypeAlias(ta) => {
|
||||||
type_aliases.insert((module.name.clone(), ta.alias.clone()), ta);
|
type_aliases.insert((module.name.clone(), ta.alias.clone()), ta);
|
||||||
}
|
}
|
||||||
|
@ -385,11 +425,135 @@ impl Project {
|
||||||
Ok(programs)
|
Ok(programs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_build_outputs(&self, programs: Vec<Script>, uplc_dump: bool) -> Result<(), Error> {
|
// TODO: revisit ownership and lifetimes of data in this function
|
||||||
let assets = self.root.join("assets");
|
fn test_gen(&mut self, checked_modules: &CheckedModules) -> Result<Vec<Script>, Error> {
|
||||||
|
let mut programs = Vec::new();
|
||||||
|
let mut functions = HashMap::new();
|
||||||
|
let mut type_aliases = HashMap::new();
|
||||||
|
let mut data_types = HashMap::new();
|
||||||
|
let mut imports = HashMap::new();
|
||||||
|
let mut constants = HashMap::new();
|
||||||
|
|
||||||
|
// let mut indices_to_remove = Vec::new();
|
||||||
|
let mut tests = Vec::new();
|
||||||
|
|
||||||
|
for module in checked_modules.values() {
|
||||||
|
for (_index, def) in module.ast.definitions().enumerate() {
|
||||||
|
match def {
|
||||||
|
Definition::Fn(func) => {
|
||||||
|
functions.insert(
|
||||||
|
FunctionAccessKey {
|
||||||
|
module_name: module.name.clone(),
|
||||||
|
function_name: func.name.clone(),
|
||||||
|
},
|
||||||
|
func,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Definition::Test(func) => {
|
||||||
|
tests.push((module.name.clone(), func));
|
||||||
|
// indices_to_remove.push(index);
|
||||||
|
}
|
||||||
|
Definition::TypeAlias(ta) => {
|
||||||
|
type_aliases.insert((module.name.clone(), ta.alias.clone()), ta);
|
||||||
|
}
|
||||||
|
Definition::DataType(dt) => {
|
||||||
|
data_types.insert(
|
||||||
|
DataTypeKey {
|
||||||
|
module_name: module.name.clone(),
|
||||||
|
defined_type: dt.name.clone(),
|
||||||
|
},
|
||||||
|
dt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Definition::Use(import) => {
|
||||||
|
imports.insert((module.name.clone(), import.module.join("/")), import);
|
||||||
|
}
|
||||||
|
Definition::ModuleConstant(mc) => {
|
||||||
|
constants.insert((module.name.clone(), mc.name.clone()), mc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for index in indices_to_remove.drain(0..) {
|
||||||
|
// module.ast.definitions.remove(index);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (module_name, func_def) in tests {
|
||||||
|
let Function {
|
||||||
|
arguments,
|
||||||
|
name,
|
||||||
|
body,
|
||||||
|
..
|
||||||
|
} = func_def;
|
||||||
|
|
||||||
|
let mut generator = CodeGenerator::new(
|
||||||
|
&functions,
|
||||||
|
// &type_aliases,
|
||||||
|
&data_types,
|
||||||
|
// &imports,
|
||||||
|
// &constants,
|
||||||
|
&self.module_types,
|
||||||
|
);
|
||||||
|
|
||||||
|
let program = generator.generate(body.clone(), arguments.clone());
|
||||||
|
|
||||||
|
let script = Script::new(module_name, name.to_string(), program.try_into().unwrap());
|
||||||
|
|
||||||
|
programs.push(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(programs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_tests(&self, tests: Vec<Script>) {
|
||||||
|
// TODO: in the future we probably just want to be able to
|
||||||
|
// tell the machine to not explode on budget consumption.
|
||||||
|
let initial_budget = ExBudget {
|
||||||
|
mem: i64::MAX,
|
||||||
|
cpu: i64::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !tests.is_empty() {
|
||||||
|
self.event_listener.handle_event(Event::RunningTests);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
match test.program.eval(initial_budget) {
|
||||||
|
(Ok(..), remaining_budget, _) => {
|
||||||
|
let test_info = TestInfo {
|
||||||
|
is_passing: true,
|
||||||
|
test,
|
||||||
|
spent_budget: initial_budget - remaining_budget,
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(test_info);
|
||||||
|
}
|
||||||
|
(Err(_), remaining_budget, _) => {
|
||||||
|
let test_info = TestInfo {
|
||||||
|
is_passing: false,
|
||||||
|
test,
|
||||||
|
spent_budget: initial_budget - remaining_budget,
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(test_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.event_listener
|
||||||
|
.handle_event(Event::FinishedTests { tests: results });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_path(&self) -> PathBuf {
|
||||||
|
self.root.join("assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_build_outputs(&self, programs: Vec<Script>, uplc_dump: bool) -> Result<(), Error> {
|
||||||
for script in programs {
|
for script in programs {
|
||||||
let script_output_dir = assets.join(script.module).join(script.name);
|
let script_output_dir = self.output_path().join(script.module).join(script.name);
|
||||||
|
|
||||||
fs::create_dir_all(&script_output_dir)?;
|
fs::create_dir_all(&script_output_dir)?;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::script::Script;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uplc::machine::cost_model::ExBudget;
|
||||||
|
|
||||||
|
pub trait EventListener: std::fmt::Debug {
|
||||||
|
fn handle_event(&self, event: Event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
StartingCompilation {
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
root: PathBuf,
|
||||||
|
},
|
||||||
|
ParsingProjectFiles,
|
||||||
|
TypeChecking,
|
||||||
|
GeneratingUPLC {
|
||||||
|
output_path: PathBuf,
|
||||||
|
},
|
||||||
|
RunningTests,
|
||||||
|
FinishedTests {
|
||||||
|
tests: Vec<TestInfo>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestInfo {
|
||||||
|
pub is_passing: bool,
|
||||||
|
pub test: Script,
|
||||||
|
pub spent_budget: ExBudget,
|
||||||
|
}
|
|
@ -495,6 +495,7 @@ impl From<Term<FakeNamedDeBruijn>> for Term<NamedDeBruijn> {
|
||||||
impl Program<NamedDeBruijn> {
|
impl Program<NamedDeBruijn> {
|
||||||
pub fn eval(
|
pub fn eval(
|
||||||
&self,
|
&self,
|
||||||
|
initial_budget: ExBudget,
|
||||||
) -> (
|
) -> (
|
||||||
Result<Term<NamedDeBruijn>, crate::machine::Error>,
|
Result<Term<NamedDeBruijn>, crate::machine::Error>,
|
||||||
ExBudget,
|
ExBudget,
|
||||||
|
@ -503,7 +504,7 @@ impl Program<NamedDeBruijn> {
|
||||||
let mut machine = Machine::new(
|
let mut machine = Machine::new(
|
||||||
Language::PlutusV2,
|
Language::PlutusV2,
|
||||||
CostModel::default(),
|
CostModel::default(),
|
||||||
ExBudget::default(),
|
initial_budget,
|
||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -558,6 +559,7 @@ impl Program<NamedDeBruijn> {
|
||||||
impl Program<DeBruijn> {
|
impl Program<DeBruijn> {
|
||||||
pub fn eval(
|
pub fn eval(
|
||||||
&self,
|
&self,
|
||||||
|
initial_budget: ExBudget,
|
||||||
) -> (
|
) -> (
|
||||||
Result<Term<NamedDeBruijn>, crate::machine::Error>,
|
Result<Term<NamedDeBruijn>, crate::machine::Error>,
|
||||||
ExBudget,
|
ExBudget,
|
||||||
|
@ -565,7 +567,7 @@ impl Program<DeBruijn> {
|
||||||
) {
|
) {
|
||||||
let program: Program<NamedDeBruijn> = self.clone().into();
|
let program: Program<NamedDeBruijn> = self.clone().into();
|
||||||
|
|
||||||
program.eval()
|
program.eval(initial_budget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,17 @@ impl Default for ExBudget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for ExBudget {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
ExBudget {
|
||||||
|
mem: self.mem - rhs.mem,
|
||||||
|
cpu: self.cpu - rhs.cpu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CostModel {
|
pub struct CostModel {
|
||||||
pub machine_costs: MachineCosts,
|
pub machine_costs: MachineCosts,
|
||||||
|
|
|
@ -786,7 +786,7 @@ pub fn eval_redeemer(
|
||||||
|
|
||||||
program.eval_as(&Language::PlutusV2, costs, initial_budget)
|
program.eval_as(&Language::PlutusV2, costs, initial_budget)
|
||||||
} else {
|
} else {
|
||||||
program.eval()
|
program.eval(ExBudget::default())
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
@ -889,7 +889,7 @@ pub fn eval_redeemer(
|
||||||
|
|
||||||
program.eval_as(&Language::PlutusV2, costs, initial_budget)
|
program.eval_as(&Language::PlutusV2, costs, initial_budget)
|
||||||
} else {
|
} else {
|
||||||
program.eval()
|
program.eval(ExBudget::default())
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
use aiken/builtin
|
||||||
|
|
||||||
|
test bar() {
|
||||||
|
builtin.length_of_bytearray(#[2, 2, 3]) == 3
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use sample
|
use sample
|
||||||
|
|
||||||
pub fn spend(datum: sample.Datum, rdmr: sample.Redeemer, _ctx: Nil) -> Bool {
|
pub fn spend(datum: sample.Datum, rdmr: sample.Redeemer, _ctx: Nil) -> Bool {
|
||||||
let x = #(datum, #[244])
|
let _x = #(datum, rdmr, #[244])
|
||||||
|
|
||||||
let y = [#(#[222], #[222]), #(#[233], #[52])]
|
let y = [#(#[222], #[222]), #(#[233], #[52])]
|
||||||
|
|
||||||
|
@ -11,3 +11,7 @@ pub fn spend(datum: sample.Datum, rdmr: sample.Redeemer, _ctx: Nil) -> Bool {
|
||||||
|
|
||||||
z == #(#[222], #[222])
|
z == #(#[222], #[222])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test foo() {
|
||||||
|
1 + 1 == 2
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue