blob: 3239adeba59a76f35cad54750709be061ae29f01 [file] [edit]
// Copyright 2026 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use starlark::{
environment::{FrozenModule, GlobalsBuilder},
values::UnpackValue,
};
use types::{EvaluatorContextExt, Label, PathResolver, UnpackedOwnedValue};
use crate::{register_globals, FakeEvalContext};
type GlobalsConfig = Box<dyn Fn(&mut GlobalsBuilder)>;
/// A simple wrapper around starlark::Assert that provides fake evaluation
/// contexts.
pub struct Assert {
assert: starlark::assert::Assert<'static>,
context: Box<FakeEvalContext>,
globals_configs: Vec<GlobalsConfig>,
}
impl Default for Assert {
fn default() -> Self {
Self::new(FakeEvalContext::default())
}
}
impl Assert {
/// Creates a new `Assert` helper instance with the given context.
pub fn new(context: FakeEvalContext) -> Self {
let mut assert = starlark::assert::Assert::new();
// By default, starlark runs the code 3 times (with always GC, auto GC, and
// disabled GC) to check for GC bugs. Because the EvalContext can be
// mutated by the evaluator, running 3 times could cause state mutations
// to happen three times, messing with tests. Calling `always_gc()`
// forces the framework to run the code only once (specifically,
// under the "always GC" configuration), ensuring state is mutated only once.
assert.always_gc();
let context = Box::new(context);
let context_ptr = &*context as *const FakeEvalContext;
assert.setup_eval(move |eval| {
// Safety: The context is owned by Assert, which outlives the evaluator run.
// Since all evaluation methods on Assert require `&mut self`, this guarantees
// exclusive access to `context` when the evaluator runs, so dereferencing
// this pointer is safe and does not alias.
eval.set_context(unsafe { &*context_ptr });
});
let mut s = Self {
assert,
context,
globals_configs: vec![],
};
s.modify_globals(register_globals);
s
}
/// Adds a modifier to globals.
/// This modifier is applied after all existing modifiers.
pub fn modify_globals(&mut self, f: impl Fn(&mut GlobalsBuilder) + 'static) {
self.globals_configs.push(Box::new(f));
// globals_add overwrites all previous calls to globals_add.
self.assert.globals_add(|builder| {
for config in &self.globals_configs {
config(builder);
}
});
}
pub fn module_add(&mut self, module: FrozenModule) {
let name = module.frozen_heap().name().unwrap().to_string();
self.assert.module_add(&name, module);
}
pub fn load_file(&self, label: &str) -> String {
let loader = PathResolver::new_for_testing();
let parsed = Label::parse(label, types::PackageRef::root()).unwrap();
let path = loader.absolute_path(parsed.package(), parsed.name());
std::fs::read_to_string(path).unwrap()
}
pub fn load_module(&mut self, label: &str) -> FrozenModule {
let content = self.load_file(label);
self.assert.module(label, &content)
}
/// Returns a read-only reference to the fake evaluation context.
pub fn context(&self) -> &FakeEvalContext {
&self.context
}
/// Asserts that the result of evaluating code is equal to expected.
#[track_caller]
pub fn eq<T>(&mut self, code: &str, expected: T)
where
T: PartialEq + std::fmt::Debug + for<'v> UnpackValue<'v> + 'static,
{
assert_eq!(*self.eval::<T>(code), expected);
}
/// Evaluates code and unpacks it to a given type.
#[track_caller]
pub fn eval<T>(&mut self, code: &str) -> UnpackedOwnedValue<T>
where
T: for<'v> UnpackValue<'v> + 'static,
{
let owned_val = self.assert.pass(code);
UnpackedOwnedValue::<T>::try_from(owned_val).unwrap()
}
/// Asserts that the two pieces of code produce something equivalent.
#[track_caller]
pub fn equivalent(&mut self, lhs_code: &str, rhs_code: &str) {
let lhs_val = self.assert.pass(lhs_code);
let rhs_val = self.assert.pass(rhs_code);
assert_eq!(lhs_val.value(), rhs_val.value());
}
// We explicitly implement `pass`, `fail`, and `fails` with `&mut self`
// signatures. The inherited methods on `starlark::assert::Assert` only
// take `&self`, which would bypass the borrow checker and allow running
// the evaluator while holding an active context borrow (leading to UB).
// Exposing them as `&mut self` methods on the wrapper statically prevents this.
/// Evaluates code and returns the Starlark value.
#[track_caller]
pub fn pass(&mut self, code: &str) -> starlark::values::OwnedFrozenValue {
self.assert.pass(code)
}
/// Asserts that freezing the evaluated module fails with the expected error.
#[track_caller]
pub fn fail_to_freeze(&mut self, code: &str, expected_error: &str) -> starlark::Error {
self.assert.fail_to_freeze(code, expected_error)
}
/// Asserts that the code fails to evaluate with the expected error.
#[track_caller]
pub fn fail(&mut self, code: &str, expected_error: &str) -> starlark::Error {
self.assert.fail(code, expected_error)
}
/// Asserts that the code fails to evaluate with any of the expected errors.
#[track_caller]
pub fn fails(&mut self, code: &str, expected_errors: &[&str]) -> starlark::Error {
self.assert.fails(code, expected_errors)
}
}