Lines
94.68 %
Functions
80.95 %
Branches
100 %
use std::fs;
use std::path::PathBuf;
use nomiscript::{Annotation, Expr, Program, Reader};
fn samples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("samples")
}
fn read_sample(name: &str) -> String {
let path = samples_dir().join(name);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e))
fn parse_sample(name: &str) -> Program {
let content = read_sample(name);
Reader::parse(&content).unwrap_or_else(|e| panic!("Failed to parse {name}: {e}"))
fn count_forms(program: &Program, symbol: &str) -> usize {
program
.exprs
.iter()
.filter(|expr| {
matches!(expr, Expr::List(items) if matches!(items.first(), Some(Expr::Symbol(s)) if s == symbol))
})
.count()
fn count_top_level_exprs(program: &Program) -> usize {
program.exprs.len()
fn eval_query(query: &Expr, program: &Program) -> Option<i64> {
let items = query.as_list()?;
let func = items.first()?.as_symbol()?;
match func {
"COUNT" => {
let arg = items.get(1)?;
if let Expr::Quote(inner) = arg {
let symbol = inner.as_symbol()?;
if symbol == "EXPRS" {
Some(count_top_level_exprs(program) as i64)
} else {
Some(count_forms(program, symbol) as i64)
None
_ => None,
fn eval_test_annotation(ann: &Annotation, program: &Program) -> Result<bool, String> {
if ann.name != "test" {
return Ok(true);
let items = ann
.value
.as_list()
.ok_or_else(|| "test annotation must be a list".to_string())?;
let op = items
.first()
.and_then(|e| e.as_symbol())
.ok_or_else(|| "first element must be an operator symbol".to_string())?;
match op {
"=" => {
let lhs = items
.get(1)
.ok_or_else(|| "missing left operand".to_string())?;
let rhs = items
.get(2)
.ok_or_else(|| "missing right operand".to_string())?;
let lhs_val = eval_query(lhs, program)
.ok_or_else(|| format!("cannot evaluate query: {lhs:?}"))?;
let rhs_val = match rhs {
Expr::Number(n) => n.to_integer(),
_ => return Err(format!("expected number, got {rhs:?}")),
};
Ok(lhs_val == rhs_val)
_ => Err(format!("unknown operator: {op}")),
const SAMPLE_FILES: &[&str] = &[
"basics.nms",
"recursion.nms",
"lists.nms",
"higher_order.nms",
"finance.nms",
"strings_and_io.nms",
"let_forms.nms",
"data_structures.nms",
"map_family.nms",
];
mod sample_parsing {
use super::*;
#[test]
fn test_all_samples_match_annotations() {
for &sample in SAMPLE_FILES {
let program = parse_sample(sample);
for ann in &program.annotations {
if ann.name == "test" {
let result = eval_test_annotation(ann, &program);
match result {
Ok(true) => {}
Ok(false) => {
panic!("{}: test annotation failed: {:?}", sample, ann.value);
Err(e) => {
panic!("{sample}: error evaluating annotation: {e}");
fn test_annotation_parsing() {
let content = "; @test (= (count 'defun) 1)\n(defun foo () nil)";
let program = Reader::parse(content).unwrap();
assert_eq!(program.annotations.len(), 1);
assert_eq!(program.annotations[0].name, "test");
let result = eval_test_annotation(&program.annotations[0], &program);
assert!(result.unwrap());
fn extract_defun_name(expr: &Expr) -> Option<&str> {
if let Expr::List(items) = expr
&& let (Some(Expr::Symbol(sym)), Some(Expr::Symbol(name))) = (items.first(), items.get(1))
&& sym == "DEFUN"
{
return Some(name.as_str());
fn extract_defvar_name(expr: &Expr) -> Option<&str> {
&& sym == "DEFVAR"
mod sample_structure {
fn test_basics_has_functions() {
let program = parse_sample("basics.nms");
let defun_names: Vec<&str> = program
.filter_map(extract_defun_name)
.collect();
let defvar_names: Vec<&str> = program
.filter_map(extract_defvar_name)
assert!(defvar_names.contains(&"PI"));
assert!(defvar_names.contains(&"E"));
assert!(defun_names.contains(&"SQUARE"));
assert!(defun_names.contains(&"CUBE"));
assert!(defun_names.contains(&"ABS"));
assert!(defun_names.contains(&"MAX"));
assert!(defun_names.contains(&"MIN"));
fn test_recursion_has_factorial() {
let program = parse_sample("recursion.nms");
assert!(defun_names.contains(&"FACTORIAL"));
fn test_lists_has_map_filter_fold() {
let program = parse_sample("lists.nms");
assert!(defun_names.contains(&"MAP"));
assert!(defun_names.contains(&"FILTER"));
assert!(defun_names.contains(&"FOLD-LEFT"));
assert!(defun_names.contains(&"FOLD-RIGHT"));
fn test_higher_order_has_compose() {
let program = parse_sample("higher_order.nms");
assert!(defun_names.contains(&"COMPOSE"));
fn test_finance_has_money_functions() {
let program = parse_sample("finance.nms");
assert!(defun_names.contains(&"MAKE-MONEY"));
assert!(defun_names.contains(&"MONEY-AMOUNT"));
assert!(defun_names.contains(&"MAKE-SPLIT"));
assert!(defun_names.contains(&"MAKE-TRANSACTION"));
fn test_strings_has_multiline() {
let content = read_sample("strings_and_io.nms");
assert!(content.contains("\"\"\""));
fn test_let_forms_has_various_lets() {
let content = read_sample("let_forms.nms");
assert!(content.contains("(let "));
assert!(content.contains("(let* "));
assert!(content.contains("(letrec "));
mod all_samples {
fn test_all_samples_parse_successfully() {
let content = read_sample(sample);
let result = Reader::parse(&content);
assert!(
result.is_ok(),
"Failed to parse {}: {:?}",
sample,
result.err()
);
let program = result.unwrap();
!program.exprs.is_empty(),
"{sample} should have at least one expression"
fn test_total_expressions() {
let total_exprs: usize = SAMPLE_FILES
.map(|s| parse_sample(s).exprs.len())
.sum();
total_exprs > 50,
"Should have substantial test coverage, got {total_exprs}"
fn test_no_empty_samples() {
let samples_path = samples_dir();
let entries =
fs::read_dir(&samples_path).unwrap_or_else(|e| panic!("Cannot read samples dir: {e}"));
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|e| e == "nms") {
let content = fs::read_to_string(&path).unwrap();
!content.trim().is_empty(),
"{} should not be empty",
path.display()
let program = Reader::parse(&content).unwrap();
"{} should parse to at least one expression",
fn test_all_samples_have_annotations() {
let test_annotations: Vec<_> = program
.annotations
.filter(|a| a.name == "test")
!test_annotations.is_empty(),
"{sample} should have @test annotations"
mod comments {
fn test_samples_with_comments_parse() {
let content = read_sample("lists.nms");
assert!(content.contains("; Uses an accumulator"));
assert!(!program.exprs.is_empty());
fn test_inline_comments_ignored() {
assert!(content.contains("; Solve ax^2"));
assert!(eval_test_annotation(ann, &program).unwrap());
mod error_samples {
fn test_unclosed_paren_fails() {
let bad_code = "(defun x 10";
assert!(Reader::parse(bad_code).is_err());
fn test_unmatched_paren_fails() {
let bad_code = "(defvar x 10))";
fn test_unclosed_string_fails() {
let bad_code = r#"(defvar x "hello)"#;
fn test_empty_input_succeeds() {
let empty = "";
let result = Reader::parse(empty);
assert!(result.is_ok());
assert!(result.unwrap().exprs.is_empty());
fn test_only_comments_succeeds() {
let only_comments = "; just comments\n; more comments";
let result = Reader::parse(only_comments);