DEV: Integration tests all pass now.

This commit is contained in:
Jeremy Wall 2019-09-02 10:10:24 -05:00
parent 2b64c2b4e0
commit b3fd37a6b5
9 changed files with 171 additions and 69 deletions

View File

@ -69,4 +69,15 @@ let name = "foo";
assert t.ok{ assert t.ok{
test = (name) in {foo="foo"}, test = (name) in {foo="foo"},
desc = "bareword collisions with field names still works for `in` operator", desc = "bareword collisions with field names still works for `in` operator",
};
assert t.ok{
test = "foo" in ["foo"],
desc = "List presence checks work",
};
let foo_string = "foo";
assert t.ok{
test = foo_string in ["foo"],
desc = "List presence checks work",
}; };

View File

@ -190,7 +190,7 @@ where
pub fn eval_stmts(&mut self, ast: Vec<Statement>) -> BuildResult { pub fn eval_stmts(&mut self, ast: Vec<Statement>) -> BuildResult {
// We should probably stash this in an op_cache somewhere? // We should probably stash this in an op_cache somewhere?
let ops = translate::AST::translate(ast, &self.working_dir); let ops = translate::AST::translate(ast, &self.working_dir);
let mut vm = VM::new(Rc::new(ops), self.environment.clone()); let mut vm = VM::new(Rc::new(ops), self.environment.clone(), &self.working_dir);
if self.validate_mode { if self.validate_mode {
vm.enable_validate_mode(); vm.enable_validate_mode();
} }
@ -227,7 +227,11 @@ where
&mut ops_map, &mut ops_map,
&self.working_dir, &self.working_dir,
); );
let mut vm = VM::new(Rc::new(ops_map), self.environment.clone()); let mut vm = VM::new(
Rc::new(ops_map),
self.environment.clone(),
&self.working_dir,
);
if self.validate_mode { if self.validate_mode {
vm.enable_validate_mode(); vm.enable_validate_mode();
} }

View File

@ -22,7 +22,7 @@ use super::OpPointer;
/// A Cache of Op codes. /// A Cache of Op codes.
pub struct Ops { pub struct Ops {
ops: BTreeMap<String, Rc<PositionMap>>, ops: BTreeMap<PathBuf, Rc<PositionMap>>,
} }
impl Ops { impl Ops {
@ -32,12 +32,12 @@ impl Ops {
} }
} }
pub fn entry<'a, S: Into<String>>(&'a mut self, path: S) -> Entry<'a> { pub fn entry<'a, P: Into<PathBuf>>(&'a mut self, path: P) -> Entry<'a> {
Entry(self.ops.entry(path.into())) Entry(self.ops.entry(path.into()))
} }
} }
pub struct Entry<'a>(btree_map::Entry<'a, String, Rc<PositionMap>>); pub struct Entry<'a>(btree_map::Entry<'a, PathBuf, Rc<PositionMap>>);
impl<'a> Entry<'a> { impl<'a> Entry<'a> {
pub fn get_pointer_or_else<F: FnOnce() -> Result<PositionMap, Error>, P: Into<PathBuf>>( pub fn get_pointer_or_else<F: FnOnce() -> Result<PositionMap, Error>, P: Into<PathBuf>>(

View File

@ -70,14 +70,18 @@ impl<Stdout: Write, Stderr: Write> Environment<Stdout, Stderr> {
self.val_cache.insert(path.clone(), val); self.val_cache.insert(path.clone(), val);
} }
pub fn get_ops_for_path(&mut self, path: &String) -> Result<OpPointer, Error> { pub fn get_ops_for_path<P>(&mut self, path: P) -> Result<OpPointer, Error>
self.op_cache.entry(path).get_pointer_or_else( where
P: Into<PathBuf> + Clone,
{
let path_copy = path.clone();
self.op_cache.entry(path.clone()).get_pointer_or_else(
|| { || {
// FIXME(jwall): We need to do proper error handling here. // FIXME(jwall): We need to do proper error handling here.
let p = PathBuf::from(&path); let p = path.into();
let root = p.parent().unwrap(); let root = p.parent().unwrap();
// first we read in the file // first we read in the file
let mut f = File::open(&path)?; let mut f = File::open(&p)?;
// then we parse it // then we parse it
let mut contents = String::new(); let mut contents = String::new();
f.read_to_string(&mut contents)?; f.read_to_string(&mut contents)?;
@ -88,7 +92,7 @@ impl<Stdout: Write, Stderr: Write> Environment<Stdout, Stderr> {
let ops = super::translate::AST::translate(stmts, &root); let ops = super::translate::AST::translate(stmts, &root);
Ok(ops) Ok(ops)
}, },
&path, path_copy,
) )
} }

View File

@ -58,7 +58,6 @@ macro_rules! decorate_call {
match $result { match $result {
Ok(v) => Ok(v), Ok(v) => Ok(v),
Err(mut e) => { Err(mut e) => {
dbg!(&$pos);
e.push_call_stack($pos.clone()); e.push_call_stack($pos.clone());
Err(e) Err(e)
} }

View File

@ -16,6 +16,7 @@ use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::fmt::Debug;
use regex::Regex; use regex::Regex;
@ -29,7 +30,6 @@ use Composite::{List, Tuple};
use Primitive::{Bool, Empty, Int, Str}; use Primitive::{Bool, Empty, Int, Str};
pub struct Builtins { pub struct Builtins {
working_dir: PathBuf,
import_path: Vec<PathBuf>, import_path: Vec<PathBuf>,
validate_mode: bool, validate_mode: bool,
} }
@ -37,12 +37,7 @@ pub struct Builtins {
impl Builtins { impl Builtins {
pub fn new() -> Self { pub fn new() -> Self {
// FIXME(jwall): This should probably be injected in. // FIXME(jwall): This should probably be injected in.
Self::with_working_dir(std::env::current_dir().unwrap())
}
pub fn with_working_dir<P: Into<PathBuf>>(path: P) -> Self {
Self { Self {
working_dir: path.into(),
import_path: Vec::new(), import_path: Vec::new(),
validate_mode: false, validate_mode: false,
} }
@ -50,7 +45,6 @@ impl Builtins {
pub fn clone(&self) -> Self { pub fn clone(&self) -> Self {
Self { Self {
working_dir: self.working_dir.clone(),
import_path: self.import_path.clone(), import_path: self.import_path.clone(),
validate_mode: self.validate_mode, validate_mode: self.validate_mode,
} }
@ -60,22 +54,25 @@ impl Builtins {
self.validate_mode = true; self.validate_mode = true;
} }
pub fn handle<P: AsRef<Path>, O, E>( pub fn handle<P, WP, O, E>(
&mut self, &mut self,
path: Option<P>, path: Option<P>,
h: Hook, h: Hook,
stack: &mut Vec<(Rc<Value>, Position)>, stack: &mut Vec<(Rc<Value>, Position)>,
env: Rc<RefCell<Environment<O, E>>>, env: Rc<RefCell<Environment<O, E>>>,
import_stack: &mut Vec<String>, import_stack: &mut Vec<String>,
working_dir: WP,
pos: Position, pos: Position,
) -> Result<(), Error> ) -> Result<(), Error>
where where
P: AsRef<Path>,
WP: Into<PathBuf> + Clone + Debug,
O: std::io::Write, O: std::io::Write,
E: std::io::Write, E: std::io::Write,
{ {
match h { match h {
Hook::Import => self.import(stack, env, import_stack, pos), Hook::Import => self.import(working_dir, stack, env, import_stack, pos),
Hook::Include => self.include(stack, env, pos), Hook::Include => self.include(working_dir, stack, env, pos),
Hook::Assert => self.assert(stack, env), Hook::Assert => self.assert(stack, env),
Hook::Convert => self.convert(stack, env, pos), Hook::Convert => self.convert(stack, env, pos),
Hook::Out => self.out(path, stack, env, pos), Hook::Out => self.out(path, stack, env, pos),
@ -88,15 +85,23 @@ impl Builtins {
} }
} }
fn find_file<P: Into<PathBuf>>( fn normalize_path<P, BP>(
&self, &self,
path: P, base_path: BP,
use_import_path: bool, use_import_path: bool,
pos: Position, path: P,
) -> Result<PathBuf, Error> { ) -> Result<PathBuf, Error>
where
BP: Into<PathBuf>,
P: Into<PathBuf>,
{
// Try a relative path first. // Try a relative path first.
let path = path.into(); let path = path.into();
let mut normalized = self.working_dir.clone(); // stdlib paths are special
if path.starts_with("std/") {
return Ok(path);
}
let mut normalized = base_path.into();
if path.is_relative() { if path.is_relative() {
normalized.push(&path); normalized.push(&path);
// First see if the normalized file exists or not. // First see if the normalized file exists or not.
@ -115,6 +120,23 @@ impl Builtins {
} else { } else {
normalized = path; normalized = path;
} }
Ok(normalized.canonicalize()?)
}
fn find_file<P, BP>(
&self,
base_path: BP,
path: P,
use_import_path: bool,
pos: Position,
) -> Result<PathBuf, Error>
where
P: Into<PathBuf>,
BP: Into<PathBuf>,
{
// Try a relative path first.
// FIXME(jwall): Use import paths if desired.
let normalized = self.normalize_path(base_path, use_import_path, path)?;
match normalized.canonicalize() { match normalized.canonicalize() {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(_e) => Err(Error::new( Err(_e) => Err(Error::new(
@ -124,10 +146,11 @@ impl Builtins {
} }
} }
fn get_file_as_string(&self, path: &str, pos: Position) -> Result<String, Error> { fn get_file_as_string<P: Into<PathBuf>>(&self, base_path: P, path: &str, pos: Position) -> Result<String, Error> {
let sep = format!("{}", std::path::MAIN_SEPARATOR); let sep = format!("{}", std::path::MAIN_SEPARATOR);
let raw_path = path.replace("/", &sep); let raw_path = path.replace("/", &sep);
let normalized = self.find_file(raw_path, false, pos)?; // FIXME(jwall): import paths?
let normalized = self.find_file(base_path, raw_path, false, pos)?;
// TODO(jwall): Proper error here // TODO(jwall): Proper error here
let mut f = File::open(normalized)?; let mut f = File::open(normalized)?;
let mut contents = String::new(); let mut contents = String::new();
@ -136,8 +159,9 @@ impl Builtins {
Ok(contents) Ok(contents)
} }
fn import<O, E>( fn import<P, O, E>(
&mut self, &mut self,
base_path: P,
stack: &mut Vec<(Rc<Value>, Position)>, stack: &mut Vec<(Rc<Value>, Position)>,
env: Rc<RefCell<Environment<O, E>>>, env: Rc<RefCell<Environment<O, E>>>,
import_stack: &mut Vec<String>, import_stack: &mut Vec<String>,
@ -146,31 +170,37 @@ impl Builtins {
where where
O: std::io::Write, O: std::io::Write,
E: std::io::Write, E: std::io::Write,
P: Into<PathBuf> + Clone + Debug,
{ {
let path = stack.pop(); let path = stack.pop();
if let Some((val, path_pos)) = path { if let Some((val, path_pos)) = path {
if let &Value::P(Str(ref path)) = val.as_ref() { if let &Value::P(Str(ref path)) = val.as_ref() {
// TODO(jwall): A bit hacky we should probably change import stacks to be pathbufs.
let normalized = decorate_error!(path_pos => self.normalize_path(base_path, false, path))?;
let path = normalized.to_string_lossy().to_string();
if import_stack if import_stack
.iter() .iter()
.find(|p| *p == path) .find(|p| *p == &path)
.is_some() { .is_some() {
return Err(Error::new( return Err(Error::new(
format!("You can only have one output per file"), format!("Import cycle detected: {} in {:?}", path, import_stack),
pos)); pos));
} }
import_stack.push(path.clone()); let val = {
let mut borrowed_env = env.borrow_mut(); env.borrow_mut().get_cached_path_val(&path)
match borrowed_env.get_cached_path_val(path) { };
match val {
Some(v) => { Some(v) => {
stack.push((v, path_pos)); stack.push((v, path_pos));
} }
None => { None => {
let op_pointer = let op_pointer =
decorate_error!(path_pos => borrowed_env.get_ops_for_path(path))?; decorate_error!(path_pos => env.borrow_mut().get_ops_for_path(&normalized))?;
let mut vm = VM::with_pointer(op_pointer, env.clone()); // TODO(jwall): What if we don't have a base path?
let mut vm = VM::with_pointer(op_pointer, env.clone(), normalized.parent().unwrap());
vm.run()?; vm.run()?;
let result = Rc::new(vm.symbols_to_tuple(true)); let result = Rc::new(vm.symbols_to_tuple(true));
borrowed_env.update_path_val(&path, result.clone()); env.borrow_mut().update_path_val(&path, result.clone());
stack.push((result, pos)); stack.push((result, pos));
} }
} }
@ -181,8 +211,9 @@ impl Builtins {
unreachable!(); unreachable!();
} }
fn include<O, E>( fn include<P, O, E>(
&self, &self,
base_path: P,
stack: &mut Vec<(Rc<Value>, Position)>, stack: &mut Vec<(Rc<Value>, Position)>,
env: Rc<RefCell<Environment<O, E>>>, env: Rc<RefCell<Environment<O, E>>>,
pos: Position, pos: Position,
@ -190,6 +221,7 @@ impl Builtins {
where where
O: std::io::Write, O: std::io::Write,
E: std::io::Write, E: std::io::Write,
P: Into<PathBuf> + Clone + Debug,
{ {
let path = stack.pop(); let path = stack.pop();
let typ = stack.pop(); let typ = stack.pop();
@ -216,14 +248,14 @@ impl Builtins {
}; };
if typ == "str" { if typ == "str" {
stack.push(( stack.push((
Rc::new(P(Str(self.get_file_as_string(&path, pos.clone())?))), Rc::new(P(Str(self.get_file_as_string(base_path, &path, pos.clone())?))),
pos.clone(), pos.clone(),
)); ));
} else { } else {
stack.push(( stack.push((
Rc::new(match env.borrow().importer_registry.get_importer(&typ) { Rc::new(match env.borrow().importer_registry.get_importer(&typ) {
Some(importer) => { Some(importer) => {
let contents = self.get_file_as_string(&path, pos.clone())?; let contents = self.get_file_as_string(base_path, &path, pos.clone())?;
if contents.len() == 0 { if contents.len() == 0 {
eprintln!("including an empty file. Use NULL as the result"); eprintln!("including an empty file. Use NULL as the result");
P(Empty) P(Empty)

View File

@ -13,7 +13,10 @@
// limitations under the License. // limitations under the License.
use std::path::Path; use std::path::Path;
use crate::ast::{BinaryExprType, Expression, FormatArgs, Position, Statement, Token, Value}; use crate::ast::{
BinaryExprType, BinaryOpDef, Expression, FormatArgs, Position, PositionedItem, SelectDef,
Statement, Token, TokenType, Value,
};
use crate::ast::{FuncOpDef, TemplatePart}; use crate::ast::{FuncOpDef, TemplatePart};
use crate::build::format::{ExpressionTemplate, SimpleTemplate, TemplateParser}; use crate::build::format::{ExpressionTemplate, SimpleTemplate, TemplateParser};
use crate::build::opcode::Primitive; use crate::build::opcode::Primitive;
@ -186,18 +189,38 @@ impl AST {
ops.push(Op::Mod, def.pos); ops.push(Op::Mod, def.pos);
} }
BinaryExprType::IN => { BinaryExprType::IN => {
// Dot expressions expect the left side to be pushed first // Dot expressions expect the right side to be pushed first
Self::translate_expr(*def.right, &mut ops, root); Self::translate_expr(*def.right.clone(), &mut ops, root);
// Symbols on the left side should be converted to strings to satisfy // Symbols on the left side should be converted to strings to satisfy
// the Index operation contract. // the Index operation contract.
// FIXME(jwall): List checks. // FIXME(jwall): List checks should not use symbol translation.
match *def.left { match *def.left.clone() {
Expression::Simple(Value::Symbol(name)) => { Expression::Simple(Value::Symbol(name)) => {
Self::translate_expr( // We really just want an expression that turns a symbol
Expression::Simple(Value::Str(name)), // into a name if the subject is a tuple and doesn't
&mut ops, // otherwise
root, let new_expr = Expression::Select(SelectDef {
); val: Box::new(Expression::Binary(BinaryOpDef {
kind: BinaryExprType::IS,
right: Box::new(Expression::Simple(Value::Str(
PositionedItem::new(
"tuple".to_owned(),
def.left.pos().clone(),
),
))),
left: def.right.clone(),
pos: def.left.pos().clone(),
})),
default: Some(Box::new(Expression::Simple(Value::Symbol(
name.clone(),
)))),
tuple: vec![(
Token::new("true", TokenType::BAREWORD, def.right.pos()),
Expression::Simple(Value::Str(name)),
)],
pos: def.left.pos().clone(),
});
Self::translate_expr(new_expr, &mut ops, root);
} }
expr => { expr => {
Self::translate_expr(expr, &mut ops, root); Self::translate_expr(expr, &mut ops, root);

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use crate::ast::Position; use crate::ast::Position;
@ -45,6 +46,7 @@ where
O: std::io::Write, O: std::io::Write,
E: std::io::Write, E: std::io::Write,
{ {
working_dir: PathBuf,
stack: Vec<(Rc<Value>, Position)>, stack: Vec<(Rc<Value>, Position)>,
symbols: Stack, symbols: Stack,
import_stack: Vec<String>, import_stack: Vec<String>,
@ -54,7 +56,6 @@ where
pub last: Option<(Rc<Value>, Position)>, pub last: Option<(Rc<Value>, Position)>,
self_stack: Vec<(Rc<Value>, Position)>, self_stack: Vec<(Rc<Value>, Position)>,
reserved_words: BTreeSet<&'static str>, reserved_words: BTreeSet<&'static str>,
out_lock: bool,
} }
impl<'a, O, E> VM<O, E> impl<'a, O, E> VM<O, E>
@ -62,12 +63,21 @@ where
O: std::io::Write, O: std::io::Write,
E: std::io::Write, E: std::io::Write,
{ {
pub fn new(ops: Rc<PositionMap>, env: Rc<RefCell<Environment<O, E>>>) -> Self { pub fn new<P: Into<PathBuf>>(
Self::with_pointer(OpPointer::new(ops), env) ops: Rc<PositionMap>,
env: Rc<RefCell<Environment<O, E>>>,
working_dir: P,
) -> Self {
Self::with_pointer(OpPointer::new(ops), env, working_dir)
} }
pub fn with_pointer(ops: OpPointer, env: Rc<RefCell<Environment<O, E>>>) -> Self { pub fn with_pointer<P: Into<PathBuf>>(
ops: OpPointer,
env: Rc<RefCell<Environment<O, E>>>,
working_dir: P,
) -> Self {
Self { Self {
working_dir: working_dir.into(),
stack: Vec::new(), stack: Vec::new(),
symbols: Stack::new(), symbols: Stack::new(),
import_stack: Vec::new(), import_stack: Vec::new(),
@ -77,9 +87,14 @@ where
last: None, last: None,
self_stack: Vec::new(), self_stack: Vec::new(),
reserved_words: construct_reserved_word_set(), reserved_words: construct_reserved_word_set(),
out_lock: false,
} }
} }
pub fn to_new_pointer(mut self, ops: OpPointer) -> Self {
self.ops = ops;
self
}
pub fn with_import_stack(mut self, imports: Vec<String>) -> Self { pub fn with_import_stack(mut self, imports: Vec<String>) -> Self {
self.import_stack = imports; self.import_stack = imports;
self self
@ -89,21 +104,26 @@ where
self.runtime.enable_validate_mode(); self.runtime.enable_validate_mode();
} }
pub fn to_scoped(self, symbols: Stack) -> Self { pub fn clean_copy(&self) -> Self {
Self { Self {
working_dir: self.working_dir.clone(),
stack: Vec::new(), stack: Vec::new(),
symbols: symbols, symbols: Stack::new(),
import_stack: self.import_stack.clone(), import_stack: Vec::new(),
runtime: self.runtime.clone(), runtime: self.runtime.clone(),
ops: self.ops.clone(), ops: self.ops.clone(),
env: self.env.clone(), env: self.env.clone(),
last: self.last, last: None,
self_stack: self.self_stack, self_stack: self.self_stack.clone(),
reserved_words: self.reserved_words, reserved_words: self.reserved_words.clone(),
out_lock: self.out_lock,
} }
} }
pub fn to_scoped(mut self, symbols: Stack) -> Self {
self.symbols = symbols;
self
}
pub fn symbols_to_tuple(&self, include_mod: bool) -> Value { pub fn symbols_to_tuple(&self, include_mod: bool) -> Value {
let mut flds = Vec::new(); let mut flds = Vec::new();
let mut pos_list = Vec::new(); let mut pos_list = Vec::new();
@ -183,6 +203,9 @@ where
Op::PopSelf => self.op_pop_self()?, Op::PopSelf => self.op_pop_self()?,
}; };
} }
if let Some(p) = self.ops.path.as_ref() {
self.import_stack.push(p.to_string_lossy().to_string());
}
Ok(()) Ok(())
} }
@ -425,7 +448,7 @@ where
ref snapshot, ref snapshot,
} = f; } = f;
// use the captured scope snapshot for the function. // use the captured scope snapshot for the function.
let mut vm = Self::with_pointer(ptr.clone(), env) let mut vm = Self::with_pointer(ptr.clone(), env, std::env::current_dir()?)
.to_scoped(snapshot.clone()) .to_scoped(snapshot.clone())
.with_import_stack(import_stack.clone()); .with_import_stack(import_stack.clone());
for nm in bindings.iter() { for nm in bindings.iter() {
@ -442,7 +465,9 @@ where
fn op_new_scope(&mut self, jp: i32, ptr: OpPointer) -> Result<(), Error> { fn op_new_scope(&mut self, jp: i32, ptr: OpPointer) -> Result<(), Error> {
let scope_snapshot = self.symbols.snapshot(); let scope_snapshot = self.symbols.snapshot();
let mut vm = Self::with_pointer(ptr, self.env.clone()) let mut vm = self
.clean_copy()
.to_new_pointer(ptr)
.to_scoped(scope_snapshot) .to_scoped(scope_snapshot)
.with_import_stack(self.import_stack.clone()); .with_import_stack(self.import_stack.clone());
vm.run()?; vm.run()?;
@ -809,7 +834,7 @@ where
} }
&C(List(ref elems, _)) => { &C(List(ref elems, _)) => {
for e in elems { for e in elems {
if e == &right { if dbg!(e) == dbg!(&right) {
self.push(Rc::new(P(Bool(true))), pos)?; self.push(Rc::new(P(Bool(true))), pos)?;
return Ok(()); return Ok(());
} }
@ -905,7 +930,9 @@ where
&val_pos, &val_pos,
)?; )?;
if let Some(ptr) = pkg_ptr { if let Some(ptr) = pkg_ptr {
let mut pkg_vm = Self::with_pointer(ptr.clone(), self.env.clone()) let mut pkg_vm = self
.clean_copy()
.to_new_pointer(ptr.clone())
.with_import_stack(self.import_stack.clone()); .with_import_stack(self.import_stack.clone());
pkg_vm.run()?; pkg_vm.run()?;
let (pkg_func, val_pos) = pkg_vm.pop()?; let (pkg_func, val_pos) = pkg_vm.pop()?;
@ -919,8 +946,9 @@ where
)?; )?;
} }
// TODO(jwall): We should have a notion of a call stack here. let mut vm = self
let mut vm = Self::with_pointer(ptr.clone(), self.env.clone()) .clean_copy()
.to_new_pointer(ptr.clone())
.with_import_stack(self.import_stack.clone()); .with_import_stack(self.import_stack.clone());
vm.push(Rc::new(S("mod".to_owned())), pos.clone())?; vm.push(Rc::new(S("mod".to_owned())), pos.clone())?;
vm.push(Rc::new(C(Tuple(flds, flds_pos_list))), pos.clone())?; vm.push(Rc::new(C(Tuple(flds, flds_pos_list))), pos.clone())?;
@ -1137,6 +1165,7 @@ where
&mut self.stack, &mut self.stack,
self.env.clone(), self.env.clone(),
&mut self.import_stack, &mut self.import_stack,
&self.working_dir,
pos, pos,
) )
} }

View File

@ -38,7 +38,7 @@ assert t.equal{
assert t.ok{ assert t.ok{
test = tpl.has_fields{tpl={foo=1, bar=2}, fields=["foo", "bar"]}, test = tpl.has_fields{tpl={foo=1, bar=2}, fields=["foo", "bar"]},
desc = "tuple has fields has foo and bar fields", desc = "tuple has foo and bar fields",
}; };
assert t.not_ok{ assert t.not_ok{