#![allow(unknown_lints)]
#![allow(clippy::if_then_panic)]
use std::collections::HashMap;
use anyhow::anyhow;
use gazebo::prelude::*;
use once_cell::sync::Lazy;
use crate::{
self as starlark,
codemap::{CodeMap, FileSpanRef, Pos, Span},
collections::SmallMap,
environment::{FrozenModule, Globals, GlobalsBuilder, Module},
errors::Diagnostic,
eval::{Evaluator, ReturnFileLoader},
stdlib::PrintHandler,
syntax::{
lexer::{Lexer, Token},
AstModule, Dialect,
},
values::{none::NoneType, structs::Struct, OwnedFrozenValue, Value},
};
fn mk_environment() -> GlobalsBuilder {
GlobalsBuilder::extended().with(test_methods)
}
static GLOBALS: Lazy<Globals> = Lazy::new(|| mk_environment().build());
static ASSERT_STAR: Lazy<FrozenModule> = Lazy::new(|| {
let g = GlobalsBuilder::new()
.with_struct("assert", assert_star)
.build();
let m = Module::new();
m.frozen_heap().add_reference(g.heap());
let assert = g.get("assert").unwrap();
m.set("assert", assert);
m.set(
"freeze",
assert.get_attr("freeze", m.heap()).unwrap().unwrap(),
);
m.freeze().unwrap()
});
fn assert_equals<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
if !a.equals(b)? {
Err(anyhow!("assert_eq: expected {}, got {}", a, b))
} else {
Ok(NoneType)
}
}
fn assert_different<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
if a.equals(b)? {
Err(anyhow!("assert_ne: but {} == {}", a, b))
} else {
Ok(NoneType)
}
}
fn assert_less_than<'v>(a: Value<'v>, b: Value<'v>) -> anyhow::Result<NoneType> {
if a.compare(b)? != std::cmp::Ordering::Less {
Err(anyhow!("assert_lt: but {} >= {}", a, b))
} else {
Ok(NoneType)
}
}
#[derive(Clone, Copy, Dupe, Debug)]
enum GcStrategy {
Never, Auto, Always, }
#[starlark_module]
fn assert_star(builder: &mut crate::environment::GlobalsBuilder) {
fn eq(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_equals(a, b)
}
fn ne(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_different(a, b)
}
fn lt(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_less_than(a, b)
}
fn contains(xs: Value, x: Value) -> anyhow::Result<NoneType> {
if !xs.is_in(x)? {
Err(anyhow!("assert.contains: expected {} to be in {}", x, xs))
} else {
Ok(NoneType)
}
}
fn r#true(x: Value) -> anyhow::Result<NoneType> {
assert_equals(Value::new_bool(x.to_bool()), Value::new_bool(true))
}
fn freeze<'v>(x: Value) -> anyhow::Result<Value<'v>> {
Ok(x)
}
fn fails(f: Value, _msg: &str) -> anyhow::Result<NoneType> {
match f.invoke_pos(&[], eval) {
Err(_e) => Ok(NoneType), Ok(_) => Err(anyhow!("assert.fails: didn't fail")),
}
}
}
#[starlark_module]
fn test_methods(builder: &mut GlobalsBuilder) {
const fibonacci: Vec<i32> = vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
fn hasfields<'v>() -> anyhow::Result<Struct<'v>> {
Ok(Struct::new(SmallMap::new()))
}
fn set<'v>(xs: Value) -> anyhow::Result<Value<'v>> {
Ok(xs)
}
fn assert_eq(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_equals(a, b)
}
fn assert_ne(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_different(a, b)
}
fn assert_lt(a: Value, b: Value) -> anyhow::Result<NoneType> {
assert_less_than(a, b)
}
fn assert_true(a: Value) -> anyhow::Result<NoneType> {
if !a.to_bool() {
Err(anyhow!("assertion failed"))
} else {
Ok(NoneType)
}
}
fn assert_false(a: Value) -> anyhow::Result<NoneType> {
if a.to_bool() {
Err(anyhow!("assertion failed"))
} else {
Ok(NoneType)
}
}
fn garbage_collect() -> anyhow::Result<NoneType> {
eval.trigger_gc();
Ok(NoneType)
}
fn assert_type(v: Value, ty: Value) -> anyhow::Result<NoneType> {
v.check_type(ty, Some("v"), heap)?;
Ok(NoneType)
}
fn is_type(v: Value, ty: Value) -> anyhow::Result<bool> {
v.is_type(ty, heap)
}
}
pub struct Assert<'a> {
dialect: Dialect,
modules: HashMap<String, FrozenModule>,
globals: Globals,
gc_strategy: Option<GcStrategy>,
setup_eval: Box<dyn Fn(&mut Evaluator)>,
print_handler: Option<&'a (dyn PrintHandler + 'a)>,
}
impl<'a> Assert<'a> {
pub fn new() -> Self {
Self {
dialect: Dialect::Extended,
modules: hashmap!["assert.star".to_owned() => Lazy::force(&ASSERT_STAR).dupe()],
globals: Lazy::force(&GLOBALS).dupe(),
gc_strategy: None,
setup_eval: box |_| (),
print_handler: None,
}
}
pub fn disable_gc(&mut self) {
self.gc_strategy = Some(GcStrategy::Never)
}
pub fn setup_eval(&mut self, setup: impl Fn(&mut Evaluator) + 'static) {
self.setup_eval = box setup;
}
pub fn set_print_handler(&mut self, handler: &'a (dyn PrintHandler + 'a)) {
self.print_handler = Some(handler);
}
fn with_gc<A>(&self, f: impl Fn(GcStrategy) -> A) -> A {
match self.gc_strategy {
None => {
let res = f(GcStrategy::Auto);
f(GcStrategy::Never);
f(GcStrategy::Always);
res
}
Some(x) => f(x),
}
}
fn execute<'v>(
&self,
path: &str,
program: &str,
module: &'v Module,
gc: GcStrategy,
) -> anyhow::Result<Value<'v>> {
let mut modules = HashMap::with_capacity(self.modules.len());
for (k, v) in &self.modules {
modules.insert(k.as_str(), v);
}
let loader = ReturnFileLoader { modules: &modules };
let ast = AstModule::parse(path, program.to_owned(), &self.dialect)?;
let mut eval = Evaluator::new(module);
(self.setup_eval)(&mut eval);
if let Some(print_handler) = self.print_handler {
eval.set_print_handler(print_handler);
}
let gc_always = |_span: FileSpanRef, eval: &mut Evaluator| {
eval.trigger_gc();
};
match gc {
GcStrategy::Never => eval.disable_gc(),
GcStrategy::Auto => {}
GcStrategy::Always => eval.before_stmt(&gc_always),
}
eval.set_loader(&loader);
eval.eval_module(ast, &self.globals)
}
fn execute_fail<'v>(
&self,
func: &str,
program: &str,
module: &'v Module,
gc: GcStrategy,
) -> anyhow::Error {
match self.execute("assert.bzl", program, module, gc) {
Ok(v) => panic!(
"starlark::assert::{}, didn't fail!\nCode:\n{}\nResult:\n{}\n",
func, program, v
),
Err(e) => e,
}
}
fn execute_unwrap<'v>(
&self,
func: &str,
path: &str,
program: &str,
module: &'v Module,
gc: GcStrategy,
) -> Value<'v> {
match self.execute(path, program, module, gc) {
Ok(v) => v,
Err(err) => {
Diagnostic::eprint(&err);
panic!(
"starlark::assert::{}, failed to execute!\nCode:\n{}\nGot error: {}",
func, program, err
);
}
}
}
fn execute_unwrap_true<'v>(
&self,
func: &str,
program: &str,
module: &'v Module,
gc: GcStrategy,
) {
let v = self.execute_unwrap(func, "assert.bzl", program, module, gc);
match v.unpack_bool() {
Some(true) => {}
Some(false) => panic!("starlark::assert::{}, got false!\nCode:\n{}", func, program),
None => panic!(
"starlark::assert::{}, not a bool!\nCode:\n{}\nResult\n{}",
func, program, v
),
}
}
pub fn dialect(&mut self, x: &Dialect) {
self.dialect = x.clone();
}
pub fn dialect_set(&mut self, f: impl FnOnce(&mut Dialect)) {
f(&mut self.dialect)
}
pub fn module_add(&mut self, name: &str, module: FrozenModule) {
self.modules.insert(name.to_owned(), module);
}
pub fn module(&mut self, name: &str, program: &str) -> FrozenModule {
let module = self
.with_gc(|gc| {
let module = Module::new();
self.execute_unwrap("module", &format!("{}.bzl", name), program, &module, gc);
module.freeze()
})
.expect("error freezing module");
self.module_add(name, module.dupe());
module
}
pub fn globals(&mut self, x: Globals) {
self.globals = x;
}
pub fn globals_add(&mut self, f: impl FnOnce(&mut GlobalsBuilder)) {
self.globals(mk_environment().with(f).build())
}
fn fails_with_name(&self, func: &str, program: &str, msgs: &[&str]) -> anyhow::Error {
self.with_gc(|gc| {
let module_env = Module::new();
let original = self.execute_fail(func, program, &module_env, gc);
let inner = original
.downcast_ref::<Diagnostic>()
.map_or(&original, |d| &d.message);
let err_msg = format!("{:#}", inner);
for msg in msgs {
if !err_msg.contains(msg) {
Diagnostic::eprint(&original);
panic!(
"starlark::assert::{}, failed with the wrong message!\nCode:\n{}\nError:\n{}\nMissing:\n{}\nExpected:\n{:?}",
func, program, inner, msg, msgs
)
}
}
original
})
}
}
impl<'a> Assert<'a> {
pub fn fail(&self, program: &str, msg: &str) -> anyhow::Error {
self.fails_with_name("fail", program, &[msg])
}
pub fn fails(&self, program: &str, msgs: &[&str]) -> anyhow::Error {
self.fails_with_name("fails", program, msgs)
}
pub fn pass(&self, program: &str) -> OwnedFrozenValue {
self.with_gc(|gc| {
let env = Module::new();
let res = self.execute_unwrap("pass", "assert.bzl", program, &env, gc);
env.set("_", res);
env.freeze()
.expect("error freezing module")
.get("_")
.unwrap()
})
}
pub fn pass_module(&self, program: &str) -> FrozenModule {
self.with_gc(|gc| {
let env = Module::new();
self.execute_unwrap("pass", "assert.bzl", program, &env, gc);
env.freeze().expect("error freezing module")
})
}
pub fn is_true(&self, program: &str) {
self.with_gc(|gc| {
let env = Module::new();
self.execute_unwrap_true("is_true", program, &env, gc);
})
}
pub fn all_true(&self, program: &str) {
self.with_gc(|gc| {
for s in program.lines() {
if s == "" {
continue;
}
let env = Module::new();
self.execute_unwrap_true("all_true", s, &env, gc);
}
})
}
pub fn eq(&self, lhs: &str, rhs: &str) {
self.with_gc(|gc| {
let lhs_m = Module::new();
let rhs_m = Module::new();
let lhs_v = self.execute_unwrap("eq", "lhs.bzl", lhs, &lhs_m, gc);
let rhs_v = self.execute_unwrap("eq", "rhs.bzl", rhs, &rhs_m, gc);
if lhs_v != rhs_v {
panic!(
"starlark::assert::eq, values differ!\nCode 1:\n{}\nCode 2:\n{}\nValue 1:\n{}\nValue 2\n{}",
lhs, rhs, lhs_v, rhs_v
);
}
})
}
pub fn parse_ast(&self, program: &str) -> AstModule {
match AstModule::parse("assert.bzl", program.to_owned(), &self.dialect) {
Ok(x) => x,
Err(e) => {
panic!(
"starlark::assert::parse_ast, expected parse success but failed\nCode: {}\nError: {}",
program, e
);
}
}
}
pub fn parse(&self, program: &str) -> String {
self.parse_ast(program).statement.to_string()
}
pub(crate) fn lex_tokens(&self, program: &str) -> Vec<(usize, Token, usize)> {
fn tokens(dialect: &Dialect, program: &str) -> Vec<(usize, Token, usize)> {
let codemap = CodeMap::new("assert.bzl".to_owned(), program.to_owned());
Lexer::new(program, dialect, codemap.dupe())
.collect::<Result<Vec<_>, _>>()
.unwrap_or_else(|e|
panic!(
"starlark::assert::lex_tokens, expected lex sucess but failed\nCode: {}\nError: {}",
program, e
)
)
}
fn check_spans(tokens: &[(usize, Token, usize)]) {
let mut pos = 0;
for (i, t, j) in tokens {
let span_incorrect = format!("Span of {:?} incorrect", t);
assert!(pos <= *i, "{}: {} > {}", span_incorrect, pos, i);
assert!(i <= j, "{}: {} > {}", span_incorrect, i, j);
pos = *j;
}
}
let orig = tokens(&self.dialect, program);
check_spans(&orig);
let with_r = tokens(
&self.dialect,
&program.replace("\r\n", "\n").replace('\n', "\r\n"),
);
check_spans(&with_r);
assert_eq!(
orig.map(|x| &x.1),
with_r.map(|x| &x.1),
"starlark::assert::lex_tokens, difference using CRLF newlines\nCode: {}",
program,
);
orig
}
pub fn lex(&self, program: &str) -> String {
self.lex_tokens(program).map(|x| x.1.unlex()).join(" ")
}
pub fn parse_fail(&self, contents: &str) -> anyhow::Error {
let rest = contents.replace('!', "");
assert!(
rest.len() + 2 == contents.len(),
"Must be exactly 2 ! marks around the parse error location"
);
let begin = contents.find('!').unwrap();
let end = contents[begin + 1..].find('!').unwrap() + begin;
match AstModule::parse("assert.bzl", rest, &self.dialect) {
Ok(ast) => panic!(
"Expected parse failure, but succeeded:\nContents: {}\nGot: {:?}",
contents, ast
),
Err(e) => {
if let Some(d) = e.downcast_ref::<Diagnostic>() {
if let Some(span) = &d.span {
let want_span = Span::new(Pos::new(begin as u32), Pos::new(end as u32));
if span.span == want_span {
return e; }
}
}
panic!(
"Expected diagnostic with span information, but didn't get a good span:\nContents: {}\nGot: {:?}\nWanted: {:?}",
contents,
e,
(begin, end)
)
}
}
}
}
pub fn eq(lhs: &str, rhs: &str) {
Assert::new().eq(lhs, rhs)
}
pub fn fail(program: &str, msg: &str) -> anyhow::Error {
Assert::new().fail(program, msg)
}
pub fn fails(program: &str, msgs: &[&str]) -> anyhow::Error {
Assert::new().fails(program, msgs)
}
pub fn is_true(program: &str) {
Assert::new().is_true(program)
}
pub fn all_true(expressions: &str) {
Assert::new().all_true(expressions)
}
pub fn pass(program: &str) -> OwnedFrozenValue {
Assert::new().pass(program)
}
pub fn pass_module(program: &str) -> FrozenModule {
Assert::new().pass_module(program)
}
pub fn parse(program: &str) -> String {
Assert::new().parse(program)
}
pub fn parse_ast(program: &str) -> AstModule {
Assert::new().parse_ast(program)
}
#[cfg(test)]
pub(crate) fn lex_tokens(program: &str) -> Vec<(usize, Token, usize)> {
Assert::new().lex_tokens(program)
}
pub fn lex(program: &str) -> String {
Assert::new().lex(program)
}
pub fn parse_fail(program: &str) -> anyhow::Error {
Assert::new().parse_fail(program)
}