Allow testing blueprint generation from Aiken programs
This is quite something, because now we have a testing pipeline that can also be used for testing other compiler-related stuff such as the type-checker or the code generator.
This commit is contained in:
parent
b93e14659c
commit
d2cc44e5f4
|
@ -112,6 +112,7 @@ version = "0.0.28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiken-lang",
|
"aiken-lang",
|
||||||
"askama",
|
"askama",
|
||||||
|
"assert-json-diff",
|
||||||
"dirs",
|
"dirs",
|
||||||
"fslock",
|
"fslock",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -197,6 +198,16 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assert-json-diff"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
|
|
@ -5,8 +5,8 @@ use strum::IntoEnumIterator;
|
||||||
use uplc::builtins::DefaultFunction;
|
use uplc::builtins::DefaultFunction;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{Arg, ArgName, CallArg, Function, ModuleKind, Span, TypedFunction, UnOp},
|
ast::{Arg, ArgName, CallArg, Function, ModuleKind, Span, TypedDataType, TypedFunction, UnOp},
|
||||||
builder::FunctionAccessKey,
|
builder::{DataTypeKey, FunctionAccessKey},
|
||||||
expr::TypedExpr,
|
expr::TypedExpr,
|
||||||
tipo::{
|
tipo::{
|
||||||
fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor,
|
fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor,
|
||||||
|
@ -800,6 +800,22 @@ pub fn prelude_functions(id_gen: &IdGenerator) -> HashMap<FunctionAccessKey, Typ
|
||||||
functions
|
functions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prelude_data_types(id_gen: &IdGenerator) -> HashMap<DataTypeKey, TypedDataType> {
|
||||||
|
let mut data_types = HashMap::new();
|
||||||
|
|
||||||
|
// Option
|
||||||
|
let option_data_type = TypedDataType::option(generic_var(id_gen.next()));
|
||||||
|
data_types.insert(
|
||||||
|
DataTypeKey {
|
||||||
|
module_name: "".to_string(),
|
||||||
|
defined_type: "Option".to_string(),
|
||||||
|
},
|
||||||
|
option_data_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
data_types
|
||||||
|
}
|
||||||
|
|
||||||
pub fn int() -> Arc<Type> {
|
pub fn int() -> Arc<Type> {
|
||||||
Arc::new(Type::App {
|
Arc::new(Type::App {
|
||||||
public: true,
|
public: true,
|
||||||
|
|
|
@ -11,6 +11,7 @@ authors = ["Lucas Rosa <x@rvcas.dev>", "Kasey White <kwhitemsg@gmail.com>"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aiken-lang = { path = "../aiken-lang", version = "0.0.28" }
|
aiken-lang = { path = "../aiken-lang", version = "0.0.28" }
|
||||||
askama = "0.10.5"
|
askama = "0.10.5"
|
||||||
|
assert-json-diff = "2.0.2"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
fslock = "0.2.1"
|
fslock = "0.2.1"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub mod validator;
|
||||||
use crate::{config::Config, module::CheckedModules};
|
use crate::{config::Config, module::CheckedModules};
|
||||||
use aiken_lang::uplc::CodeGenerator;
|
use aiken_lang::uplc::CodeGenerator;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use std::fmt::Debug;
|
use std::fmt::{self, Debug, Display};
|
||||||
use validator::Validator;
|
use validator::Validator;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
||||||
|
@ -14,6 +14,16 @@ pub struct Blueprint {
|
||||||
pub validators: Vec<validator::Validator>,
|
pub validators: Vec<validator::Validator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
||||||
|
pub struct Preamble {
|
||||||
|
pub title: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub version: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub license: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Blueprint {
|
impl Blueprint {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
@ -36,14 +46,11 @@ impl Blueprint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
|
impl Display for Blueprint {
|
||||||
pub struct Preamble {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
pub title: String,
|
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
f.write_str(&s)
|
||||||
pub description: Option<String>,
|
}
|
||||||
pub version: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub license: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preamble {
|
impl Preamble {
|
||||||
|
|
|
@ -60,6 +60,13 @@ impl Serialize for Validator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Validator {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
|
||||||
|
f.write_str(&s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Validator {
|
impl Validator {
|
||||||
pub fn from_checked_module(
|
pub fn from_checked_module(
|
||||||
modules: &CheckedModules,
|
modules: &CheckedModules,
|
||||||
|
@ -128,51 +135,141 @@ impl From<String> for Purpose {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::super::schema::{Constructor, Data, Schema};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{module::ParsedModule, PackageName};
|
||||||
|
use aiken_lang::{
|
||||||
|
self,
|
||||||
|
ast::{ModuleKind, TypedDataType, TypedFunction},
|
||||||
|
builder::{DataTypeKey, FunctionAccessKey},
|
||||||
|
builtins, parser,
|
||||||
|
tipo::TypeInfo,
|
||||||
|
IdGenerator,
|
||||||
|
};
|
||||||
|
use assert_json_diff::assert_json_eq;
|
||||||
use serde_json::{self, json};
|
use serde_json::{self, json};
|
||||||
use uplc::parser;
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
|
||||||
|
// be to make this struct below the actual project, and wrap it in another metadata struct
|
||||||
|
// which contains all the config and I/O stuff regarding the project.
|
||||||
|
struct TestProject {
|
||||||
|
package: PackageName,
|
||||||
|
id_gen: IdGenerator,
|
||||||
|
module_types: HashMap<String, TypeInfo>,
|
||||||
|
functions: HashMap<FunctionAccessKey, TypedFunction>,
|
||||||
|
data_types: HashMap<DataTypeKey, TypedDataType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestProject {
|
||||||
|
fn new() -> Self {
|
||||||
|
let id_gen = IdGenerator::new();
|
||||||
|
|
||||||
|
let package = PackageName {
|
||||||
|
owner: "test".to_owned(),
|
||||||
|
repo: "project".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut module_types = HashMap::new();
|
||||||
|
module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
|
||||||
|
module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
|
||||||
|
|
||||||
|
let functions = builtins::prelude_functions(&id_gen);
|
||||||
|
let data_types = builtins::prelude_data_types(&id_gen);
|
||||||
|
|
||||||
|
TestProject {
|
||||||
|
package,
|
||||||
|
id_gen,
|
||||||
|
module_types,
|
||||||
|
functions,
|
||||||
|
data_types,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(&self, source_code: &str) -> ParsedModule {
|
||||||
|
let kind = ModuleKind::Validator;
|
||||||
|
let (ast, extra) = parser::module(source_code, kind).unwrap();
|
||||||
|
let mut module = ParsedModule {
|
||||||
|
kind,
|
||||||
|
ast,
|
||||||
|
code: source_code.to_string(),
|
||||||
|
name: "test".to_owned(),
|
||||||
|
path: PathBuf::new(),
|
||||||
|
extra,
|
||||||
|
package: self.package.to_string(),
|
||||||
|
};
|
||||||
|
module.attach_doc_and_module_comments();
|
||||||
|
module
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, module: ParsedModule) -> CheckedModule {
|
||||||
|
let mut warnings = vec![];
|
||||||
|
|
||||||
|
let ast = module
|
||||||
|
.ast
|
||||||
|
.infer(
|
||||||
|
&self.id_gen,
|
||||||
|
module.kind,
|
||||||
|
&self.package.to_string(),
|
||||||
|
&self.module_types,
|
||||||
|
&mut warnings,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
CheckedModule {
|
||||||
|
kind: module.kind,
|
||||||
|
extra: module.extra,
|
||||||
|
name: module.name,
|
||||||
|
code: module.code,
|
||||||
|
package: module.package,
|
||||||
|
input_path: module.path,
|
||||||
|
ast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_validator(source_code: &str, json: serde_json::Value) {
|
||||||
|
let project = TestProject::new();
|
||||||
|
|
||||||
|
let modules = CheckedModules::singleton(project.check(project.parse(source_code)));
|
||||||
|
let mut generator = modules.new_generator(
|
||||||
|
&project.functions,
|
||||||
|
&project.data_types,
|
||||||
|
&project.module_types,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (validator, def) = modules
|
||||||
|
.validators()
|
||||||
|
.next()
|
||||||
|
.expect("source code did no yield any validator");
|
||||||
|
|
||||||
|
let validator =
|
||||||
|
Validator::from_checked_module(&modules, &mut generator, validator, def).unwrap();
|
||||||
|
|
||||||
|
println!("{}", validator);
|
||||||
|
assert_json_eq!(serde_json::to_value(&validator).unwrap(), json);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize() {
|
fn validator_1() {
|
||||||
let program = parser::program("(program 1.0.0 (con integer 42))")
|
assert_validator(
|
||||||
.unwrap()
|
r#"
|
||||||
.try_into()
|
fn spend(datum: Data, redeemer: Data, ctx: Data) {
|
||||||
.unwrap();
|
True
|
||||||
let validator = Validator {
|
|
||||||
title: "foo".to_string(),
|
|
||||||
description: Some("Lorem ipsum".to_string()),
|
|
||||||
purpose: Purpose::Spend,
|
|
||||||
datum: None,
|
|
||||||
redeemer: Annotated {
|
|
||||||
title: Some("Bar".to_string()),
|
|
||||||
description: None,
|
|
||||||
annotated: Schema::Data(Some(Data::AnyOf(vec![Constructor {
|
|
||||||
index: 0,
|
|
||||||
fields: vec![Data::Bytes.into()],
|
|
||||||
}
|
}
|
||||||
.into()]))),
|
"#,
|
||||||
},
|
|
||||||
program,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_value(&validator).unwrap(),
|
|
||||||
json!({
|
json!({
|
||||||
"title": "foo",
|
"title": "test",
|
||||||
"purpose": "spend",
|
"purpose": "spend",
|
||||||
"hash": "27dc8e44c17b4ae5f4b9286ab599fffe70e61b49dec61eaca1fc5898",
|
"hash": "cf2cd3bed32615bfecbd280618c1c1bec2198fc0f72b04f323a8a0d2",
|
||||||
"description": "Lorem ipsum",
|
"datum": {
|
||||||
"redeemer": {
|
"title": "Data",
|
||||||
"title": "Bar",
|
"description": "Any Plutus data."
|
||||||
"anyOf": [{
|
|
||||||
"dataType": "constructor",
|
|
||||||
"index": 0,
|
|
||||||
"fields": [{
|
|
||||||
"dataType": "bytes"
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
"compiledCode": "46010000481501"
|
"redeemer": {
|
||||||
|
"title": "Data",
|
||||||
|
"description": "Any Plutus data."
|
||||||
|
},
|
||||||
|
"compiledCode": "58250100002105646174756d00210872656465656d657200210363747800533357349445261601"
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::blueprint::Blueprint;
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{Definition, Function, ModuleKind, TypedDataType, TypedFunction},
|
ast::{Definition, Function, ModuleKind, TypedDataType, TypedFunction},
|
||||||
builder::{DataTypeKey, FunctionAccessKey},
|
builder::{DataTypeKey, FunctionAccessKey},
|
||||||
builtins::{self, generic_var},
|
builtins,
|
||||||
tipo::TypeInfo,
|
tipo::TypeInfo,
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
};
|
};
|
||||||
|
@ -81,20 +81,9 @@ where
|
||||||
module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
|
module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
|
||||||
module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
|
module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
|
||||||
|
|
||||||
let mut functions = HashMap::new();
|
let functions = builtins::prelude_functions(&id_gen);
|
||||||
for (access_key, func) in builtins::prelude_functions(&id_gen).into_iter() {
|
|
||||||
functions.insert(access_key.to_owned(), func);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data_types = HashMap::new();
|
let data_types = builtins::prelude_data_types(&id_gen);
|
||||||
let option_data_type = TypedDataType::option(generic_var(id_gen.next()));
|
|
||||||
data_types.insert(
|
|
||||||
DataTypeKey {
|
|
||||||
module_name: "".to_string(),
|
|
||||||
defined_type: "Option".to_string(),
|
|
||||||
},
|
|
||||||
option_data_type,
|
|
||||||
);
|
|
||||||
|
|
||||||
let config = Config::load(&root)?;
|
let config = Config::load(&root)?;
|
||||||
|
|
||||||
|
|
|
@ -244,6 +244,12 @@ impl<'a> From<&'a CheckedModules> for &'a HashMap<String, CheckedModule> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CheckedModules {
|
impl CheckedModules {
|
||||||
|
pub fn singleton(module: CheckedModule) -> Self {
|
||||||
|
let mut modules = Self::default();
|
||||||
|
modules.insert(module.name.clone(), module);
|
||||||
|
modules
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validators(&self) -> impl Iterator<Item = (&CheckedModule, &TypedFunction)> {
|
pub fn validators(&self) -> impl Iterator<Item = (&CheckedModule, &TypedFunction)> {
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
for validator in self.0.values().filter(|module| module.kind.is_validator()) {
|
for validator in self.0.values().filter(|module| module.kind.is_validator()) {
|
||||||
|
|
Loading…
Reference in New Issue