use insta::assert_snapshot;
use monty::MontyRun;
use monty_types::{CompileOptions, ExcType, MontyObject};
fn import_err(name: &str) -> monty_types::MontyException {
let code = format!("from collections import {name}");
let run = MontyRun::new(code, "test.py", vec![], CompileOptions::default()).expect("should parse");
run.run_no_limits(vec![]).expect_err("expected ImportError")
}
#[test]
fn implemented_names_import() {
let run = MontyRun::new(
"from collections import deque, Counter, defaultdict, namedtuple".to_owned(),
"test.py",
vec![],
CompileOptions::default(),
)
.expect("should parse");
assert!(
run.run_no_limits(vec![]).is_ok(),
"the implemented collections members should import without error",
);
}
#[test]
fn ordereddict_import_raises() {
let err = import_err("OrderedDict");
assert_eq!(err.exc_type(), ExcType::ImportError);
assert_snapshot!(
err.message().unwrap(),
@"cannot import name 'OrderedDict' from 'collections' (unknown location)"
);
}
#[test]
fn chainmap_import_raises() {
let err = import_err("ChainMap");
assert_eq!(err.exc_type(), ExcType::ImportError);
assert_snapshot!(
err.message().unwrap(),
@"cannot import name 'ChainMap' from 'collections' (unknown location)"
);
}
#[test]
fn user_wrappers_import_raises() {
for name in ["UserDict", "UserList", "UserString"] {
let err = import_err(name);
assert_eq!(err.exc_type(), ExcType::ImportError, "{name} should raise ImportError");
assert_eq!(
err.message().unwrap(),
format!("cannot import name '{name}' from 'collections' (unknown location)"),
);
}
}
#[test]
#[cfg(feature = "test-hooks")]
fn namedtuple_class_cycle_is_collected() {
let code = r"
import gc
from collections import namedtuple
def build():
values = []
cls = namedtuple('C', ['a'], defaults=[values])
# items = [0] holds no heap refs, so `contains_refs` is False
values.append(cls(0))
build()
gc.collect()
";
let run = MontyRun::new(code.to_owned(), "test.py", vec![], CompileOptions::default()).expect("should parse");
let freed = run.run_no_limits(vec![]).expect("should run");
let MontyObject::Int(freed) = freed else {
panic!("gc.collect() should return an int, got {freed:?}");
};
assert!(
freed > 0,
"the instance -> class -> defaults cycle should be collected, but gc.collect() freed {freed} entries",
);
}
#[test]
#[cfg(feature = "test-hooks")]
fn namedtuple_module_cycle_is_collected() {
let code = r"
import gc
from collections import namedtuple
def build():
holder = []
cls = namedtuple('C', ['a'], module=holder)
holder.append(cls)
build()
gc.collect()
";
let run = MontyRun::new(code.to_owned(), "test.py", vec![], CompileOptions::default()).expect("should parse");
let freed = run.run_no_limits(vec![]).expect("should run");
let MontyObject::Int(freed) = freed else {
panic!("gc.collect() should return an int, got {freed:?}");
};
assert!(
freed > 0,
"the class -> module -> class cycle should be collected, but gc.collect() freed {freed} entries",
);
}
fn host_value(code: &str) -> MontyObject {
let run = MontyRun::new(code.to_owned(), "test.py", vec![], CompileOptions::default()).expect("should parse");
run.run_no_limits(vec![]).expect("should run")
}
#[test]
fn deque_crosses_host_boundary_as_a_list() {
assert_eq!(
host_value("from collections import deque\ndeque([1, 2, 3])"),
MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2), MontyObject::Int(3)])
);
assert_eq!(
host_value("from collections import deque\ndeque([1, 2], maxlen=5)"),
MontyObject::List(vec![MontyObject::Int(1), MontyObject::Int(2)])
);
assert_eq!(
host_value("from collections import deque\n[deque([b'x']), 2]"),
MontyObject::List(vec![
MontyObject::List(vec![MontyObject::Bytes(b"x".to_vec())]),
MontyObject::Int(2)
])
);
}
#[test]
fn namedtuple_string_subscript_names_tuple() {
let run = MontyRun::new(
"from collections import namedtuple\nnamedtuple('P', 'x y')(1, 2)['x']".to_owned(),
"test.py",
vec![],
CompileOptions::default(),
)
.expect("should parse");
let err = run.run_no_limits(vec![]).expect_err("expected TypeError");
assert_snapshot!(err.message().expect("TypeError carries a message"), @"tuple indices must be integers, not 'str'");
}