sashite-sin 1.0.0

Style Identifier Notation (SIN): a compact, ASCII-only, no_std token encoding a player's side and style in abstract strategy board games.
Documentation
//! Rejection tests for SIN v1.0.0.
//!
//! `conformance.rs` proves the parser accepts exactly the right *set* of
//! strings. This file pins down *which* [`ParseError`] each class of malformed
//! input yields, so the documented error mapping cannot drift, and verifies
//! every public entry point rejects identically.
//!
//! The mapping is deliberately simple: an empty input is [`ParseError::Empty`],
//! a single non-letter byte is [`ParseError::InvalidLetter`], and anything two
//! bytes or longer — which includes every non-ASCII character, since such
//! characters are always multi-byte — is [`ParseError::TooLong`].

use sashite_sin::{Identifier, ParseError};

/// Malformed inputs paired with the exact error they must produce.
const REJECTED: &[(&str, ParseError)] = &[
    // Empty.
    ("", ParseError::Empty),
    // Length 1 — a lone non-letter cannot be an abbreviation.
    ("+", ParseError::InvalidLetter),
    ("-", ParseError::InvalidLetter),
    ("^", ParseError::InvalidLetter),
    ("1", ParseError::InvalidLetter),
    ("0", ParseError::InvalidLetter),
    (" ", ParseError::InvalidLetter),
    ("\n", ParseError::InvalidLetter),
    ("\t", ParseError::InvalidLetter),
    // Length 1 — the bytes bracketing the letter ranges (`@[` around `A`–`Z`,
    // `` ` `` and `{` around `a`–`z`) must not be mistaken for letters.
    ("@", ParseError::InvalidLetter),
    ("[", ParseError::InvalidLetter),
    ("`", ParseError::InvalidLetter),
    ("{", ParseError::InvalidLetter),
    // Length 2 — a token is exactly one character, so any second byte is too
    // many, regardless of what it is.
    ("CC", ParseError::TooLong),
    ("Kk", ParseError::TooLong),
    ("WW", ParseError::TooLong),
    ("c1", ParseError::TooLong),
    ("+C", ParseError::TooLong),
    ("C^", ParseError::TooLong),
    (" C", ParseError::TooLong), // leading whitespace
    ("C ", ParseError::TooLong), // trailing whitespace
    ("K\n", ParseError::TooLong),
    ("  ", ParseError::TooLong),
    // Length ≥ 3 — rejected on the same structural length check.
    ("abc", ParseError::TooLong),
    ("WWWW", ParseError::TooLong),
    ("    ", ParseError::TooLong),
    ("hello world", ParseError::TooLong),
    // Non-ASCII: a multi-byte character is never one byte, so it is always
    // TooLong.
    ("é", ParseError::TooLong),  // 2 bytes
    ("", ParseError::TooLong),  // 3 bytes
    ("", ParseError::TooLong), // letter + 2-byte char = 3 bytes
    ("🨀", ParseError::TooLong),  // 4 bytes
];

#[test]
fn every_entry_point_agrees_on_rejection() {
    for &(input, expected) in REJECTED {
        assert_eq!(Identifier::parse(input), Err(expected), "parse {input:?}");
        // `str::parse` exercises the `FromStr` implementation.
        assert_eq!(
            input.parse::<Identifier>(),
            Err(expected),
            "FromStr {input:?}"
        );
        assert_eq!(
            Identifier::try_from(input),
            Err(expected),
            "TryFrom<&str> {input:?}"
        );
        // The byte entry point must agree on the same (valid UTF-8) bytes.
        assert_eq!(
            Identifier::try_from(input.as_bytes()),
            Err(expected),
            "TryFrom<&[u8]> {input:?}"
        );
        assert!(!Identifier::is_valid(input), "is_valid {input:?}");
    }
}

#[test]
fn spec_section_6_4_invalid_forms_map_to_documented_errors() {
    // Every row of the specification's "invalid token examples" table (§6.4),
    // with the exact variant each must produce.
    assert_eq!(Identifier::parse(""), Err(ParseError::Empty));
    assert_eq!(Identifier::parse("CC"), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse("c1"), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse("+C"), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse("C^"), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse(" C"), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse("C "), Err(ParseError::TooLong));
    assert_eq!(Identifier::parse("1"), Err(ParseError::InvalidLetter));
    assert_eq!(Identifier::parse("é"), Err(ParseError::TooLong));
}

#[test]
fn error_messages_are_nonempty_distinct_and_usable_as_std_error() {
    let variants = [
        ParseError::Empty,
        ParseError::TooLong,
        ParseError::InvalidLetter,
    ];

    let mut messages: Vec<String> = variants.iter().map(ToString::to_string).collect();
    assert!(messages.iter().all(|m| !m.is_empty()));

    messages.sort();
    messages.dedup();
    assert_eq!(
        messages.len(),
        variants.len(),
        "Display messages must be distinct"
    );

    // The error type integrates with the standard error trait.
    let as_error: &dyn std::error::Error = &ParseError::Empty;
    assert!(!as_error.to_string().is_empty());
}