#![cfg(feature = "grammar")]
use pest::Parser;
use pest_derive::Parser;
#[derive(Parser)]
#[grammar = "../grammar/readcon.pest"]
pub struct ConGrammar;
pub fn parse_surface(input: &str) -> Result<(), pest::error::Error<Rule>> {
ConGrammar::parse(Rule::file, input).map(|_| ())
}
pub fn accepts(input: &str) -> bool {
parse_surface(input).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn fixture(name: &str) -> String {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources/test")
.join(name);
fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
#[test]
fn grammar_accepts_core_fixtures() {
for name in [
"tiny_cuh2.con",
"cuh2.con",
"sulfolene.con",
"tiny_multi_cuh2.con",
"tiny_cuh2_vel_forces.con",
"tiny_cuh2_forces.con",
"tiny_cuh2.convel",
"tiny_multi_cuh2.convel",
"tiny_cuh2_charges_spins_magmoms.con",
"tiny_cuh2_displacements.con",
"tiny_cuh2_spreads.con",
] {
let text = fixture(name);
assert!(
accepts(&text),
"grammar rejected fixture {name}: {:?}",
parse_surface(&text).err()
);
}
}
#[test]
fn grammar_lockstep_hand_parser_accepts_same_fixtures() {
use crate::iterators::ConFrameIterator;
for name in [
"tiny_cuh2.con",
"tiny_cuh2_vel_forces.con",
"tiny_cuh2_charges_spins_magmoms.con",
"tiny_cuh2_displacements.con",
"tiny_cuh2_spreads.con",
"tiny_multi_cuh2.con",
] {
let text = fixture(name);
assert!(accepts(&text), "PEG reject {name}");
let frames: Result<Vec<_>, _> = ConFrameIterator::new(&text).collect();
assert!(
frames.as_ref().map(|f| !f.is_empty()).unwrap_or(false),
"hand parser failed {name}: {frames:?}"
);
}
}
#[test]
fn grammar_rejects_obvious_garbage() {
assert!(!accepts(""));
assert!(!accepts("not a con file\n"));
assert!(!accepts("only one line"));
}
#[test]
fn grammar_file_ships_eight_section_kinds_and_scalar_rows() {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("grammar/readcon.pest");
assert!(p.is_file(), "expected shipped grammar at {}", p.display());
let body = fs::read_to_string(&p).unwrap();
assert!(body.contains("file = {"), "grammar should define file rule");
assert!(body.contains("Coordinates of Component"));
for kind in [
"Velocities",
"Forces",
"Energies",
"Charges",
"Spins",
"Magmoms",
"Displacements",
"Spreads",
] {
assert!(
body.contains(&format!("\"{kind}\"")),
"grammar missing section kind {kind}"
);
}
assert!(
body.contains("scalar_atom_line"),
"grammar must define scalar section rows"
);
assert!(
body.contains("vector_atom_line"),
"grammar must define vector section rows"
);
}
#[test]
fn grammar_accepts_scalar_and_vector_kind_snippets() {
let scalar = "\
Generated by test
{\"con_spec_version\":2,\"sections\":[\"charges\"]}
10.0 10.0 10.0
90.0 90.0 90.0
0 0
0 0
1
1
1.0
H
Coordinates of Component 1
0.0 0.0 0.0 0 0
H
Charges of Component 1
0.5 0 0
";
assert!(
accepts(scalar),
"scalar charges: {:?}",
parse_surface(scalar).err()
);
let vector = "\
Generated by test
{\"con_spec_version\":2,\"sections\":[\"magmoms\"]}
10.0 10.0 10.0
90.0 90.0 90.0
0 0
0 0
1
1
1.0
H
Coordinates of Component 1
0.0 0.0 0.0 0 0
H
Magmoms of Component 1
0.0 0.0 1.0 0 0
";
assert!(
accepts(vector),
"vector magmoms: {:?}",
parse_surface(vector).err()
);
let displacement = "\
Generated by test
{\"con_spec_version\":2,\"sections\":[\"displacements\"]}
10.0 10.0 10.0
90.0 90.0 90.0
0 0
0 0
1
1
1.0
H
Coordinates of Component 1
0.0 0.0 0.0 0 0
H
Displacements of Component 1
0.1 -0.2 0.3 0 0
";
assert!(
accepts(displacement),
"vector displacements: {:?}",
parse_surface(displacement).err()
);
let spread = "\
Generated by test
{\"con_spec_version\":2,\"sections\":[\"spreads\"]}
10.0 10.0 10.0
90.0 90.0 90.0
0 0
0 0
1
1
1.0
H
Coordinates of Component 1
0.0 0.0 0.0 0 0
H
Spreads of Component 1
0.1 0.2 0.3 0 0
";
assert!(
accepts(spread),
"vector spreads: {:?}",
parse_surface(spread).err()
);
}
fn hand_class(text: &str) -> String {
use crate::iterators::ConFrameIterator;
match ConFrameIterator::new(text).next() {
Some(Ok(_)) => "ok".into(),
Some(Err(err)) => err.class_name().into(),
None => "empty".into(),
}
}
#[test]
fn grammar_and_hand_parser_share_an_accept_reject_list() {
let ok_float = "\
PRE
{\"con_spec_version\":2}
10 10 10
90 90 90
A
B
1
1
1.0
H
coordinates of component 1
+1.5 .25 1e-1 0 3
\n";
assert!(accepts(ok_float), "{:?}", parse_surface(ok_float).err());
assert_eq!(hand_class(ok_float), "ok");
let cases = [
(
"\u{feff}PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n1\n1\n1\nH\nCoordinates of Component 1\n0 0 0 0 0\n",
false,
),
(
"PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n1\n1\n1\nH\nCoordinates of Component 1\n0 0 0 -1 0\n",
false,
),
(
"PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n1\n1\n1\nH\nCoordinates of Component 1\n0 0 0 0.0 0\n",
false,
),
(
"PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n0\n\n",
true,
),
(
"PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n0\n1\n1\n",
false,
),
(
"PRE\n{\"con_spec_version\":2}\n10 10 10\n90 90 90\nA\nB\n-1\n1\n1\n",
false,
),
("", false),
];
for (text, want) in cases {
assert_eq!(accepts(text), want, "PEG {text:?}");
assert_eq!(hand_class(text) == "ok", want, "hand {text:?}");
}
let nan = "\
PRE
{\"con_spec_version\":2}
10 10 10
90 90 90
A
B
1
1
1.0
H
Coordinates of Component 1
nan 0 0 0 0
";
assert!(
accepts(nan),
"PEG keeps the non-finite token for the semantic layer"
);
assert_eq!(hand_class(nan), "NonFiniteValue");
let unknown = "\
PRE
{\"con_spec_version\":3,\"con_spec_minor\":1,\"units\":{\"length\":\"angstrom\",\"energy\":\"eV\"},\"sections\":[\"widgets\"]}
10 10 10
90 90 90
A
B
1
1
1.0
H
Coordinates of Component 1
0 0 0 0 0
H
Widgets of Component 1
1 2 3 0 0
";
assert!(accepts(unknown), "{:?}", parse_surface(unknown).err());
assert_eq!(hand_class(unknown), "UnknownSection");
}
}