sashite-feen 0.1.0

Field Expression Encoding Notation (FEEN): a compact, ASCII-only, no_std, zero-allocation validator and encoder for board-game positions in abstract strategy games, built on EPIN and SIN.
Documentation
//! Integration tests for full-position parsing through the public API.
//!
//! Each valid case is summarized as a tuple
//! `(dimension sizes, square count, piece count, side to move, active style)`
//! reconstructed from the public accessors, so the assertions exercise the
//! observable surface of [`Feen`] rather than any internal helper.

use sashite_feen::{Feen, ParseError, Side};

fn summary(s: &str) -> Result<(Vec<u8>, u32, u32, Side, char), ParseError> {
    let f = Feen::parse(s)?;
    Ok((
        f.shape().dimensions().to_vec(),
        f.square_count(),
        f.piece_count(),
        f.active_side(),
        f.active_style().to_char(),
    ))
}

fn err(s: &str) -> ParseError {
    Feen::parse(s).unwrap_err()
}

// ---------- valid full positions ----------

#[test]
fn chess_start() {
    let s = "-rnbqk^bn-r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/-RNBQK^BN-R / W/w";
    assert_eq!(summary(s).unwrap(), (vec![8, 8], 64, 32, Side::First, 'W'));
}

#[test]
fn shogi_start() {
    let s = "lnsgk^gsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGK^GSNL / J/j";
    assert_eq!(summary(s).unwrap(), (vec![9, 9], 81, 40, Side::First, 'J'));
}

#[test]
fn xiangqi_start() {
    let s = "rheag^aehr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RHEAG^AEHR / C/c";
    assert_eq!(summary(s).unwrap(), (vec![10, 9], 90, 32, Side::First, 'C'));
}

#[test]
fn empty_board() {
    let s = "8/8/8/8/8/8/8/8 / W/w";
    assert_eq!(summary(s).unwrap(), (vec![8, 8], 64, 0, Side::First, 'W'));
}

#[test]
fn with_hands() {
    let s = "8/8/8/8/8/8/8/8 3P2B/3p2b W/w";
    assert_eq!(summary(s).unwrap(), (vec![8, 8], 64, 10, Side::First, 'W'));
}

#[test]
fn one_dimensional() {
    let s = "k^+p4+PK^ / C/c";
    assert_eq!(summary(s).unwrap(), (vec![8], 8, 4, Side::First, 'C'));
}

#[test]
fn second_to_move() {
    let s = "8/8/8/8/8/8/8/8 / w/W";
    assert_eq!(summary(s).unwrap(), (vec![8, 8], 64, 0, Side::Second, 'w'));
}

// ---------- cardinality ----------

#[test]
fn too_many_pieces() {
    let s = "K^k^ 2K^/2k^ J/j"; // n = 2, p = 2 + 4 = 6
    assert_eq!(err(s), ParseError::TooManyPieces);
}

#[test]
fn exactly_full_board_is_ok() {
    let s = "k^ / J/j"; // 1 square, 1 piece on board, empty hands
    assert_eq!(summary(s).unwrap(), (vec![1], 1, 1, Side::First, 'J'));
}

// ---------- field-count / whitespace structure ----------

#[test]
fn empty_input() {
    assert_eq!(err(""), ParseError::FieldCount);
}

#[test]
fn two_fields() {
    assert_eq!(err("8/8 /"), ParseError::FieldCount);
}

#[test]
fn four_fields() {
    assert_eq!(err("8 / W/w extra"), ParseError::FieldCount);
}

#[test]
fn leading_space() {
    assert_eq!(err(" 8/8/8/8/8/8/8/8 / W/w"), ParseError::FieldCount);
}

#[test]
fn trailing_space() {
    assert_eq!(err("8/8/8/8/8/8/8/8 / W/w "), ParseError::FieldCount);
}

#[test]
fn double_space() {
    assert_eq!(err("8/8/8/8/8/8/8/8  / W/w"), ParseError::FieldCount);
}

// ---------- bounds ----------

#[test]
fn non_ascii() {
    assert_eq!(err("8/8/8/8/8/8/8/8 / W/é"), ParseError::NonAscii);
}

#[test]
fn too_long() {
    let big = "1".repeat(5000);
    let s = format!("{big} / W/w");
    assert_eq!(err(&s), ParseError::InputTooLong);
}

// ---------- per-field error propagation ----------

#[test]
fn placement_error_propagates() {
    assert_eq!(err("rkr//PPPP / W/w"), ParseError::DimensionalCoherence);
}

#[test]
fn hands_error_propagates() {
    assert_eq!(err("1 PP/ W/w"), ParseError::HandNotAggregated);
}

#[test]
fn style_error_propagates() {
    assert_eq!(err("1 / W/W"), ParseError::StylesSameCase);
}

#[test]
fn newline_in_field_rejected() {
    // a trailing newline lands inside field 3 and fails SIN parsing
    assert_eq!(
        err("8/8/8/8/8/8/8/8 / W/w\n"),
        ParseError::InvalidStyleToken
    );
}