feat: unified testing framework — unit and e2e same syntax, seed-based graph testing, debugger infrastructure
- New crate el-test: test discovery, in-memory graph seeding, assertion evaluator, TestRunner, TestReport with human/JSON/JUnit XML output - New keywords: test, seed, assert, target — fully integrated into lexer, parser, codegen, type-checker - Parser extensions: TestDef, SeedStmt, Assert AST nodes; seed blocks handle type: as field name (keyword-as-ident in seed context) - Debugger: DebugEvent, Debugger, StepMode, StackFrame in el-compiler — breakpoints, step-over, step-into, step-out - CLI: el test-file <file.el> runs tests; el test integrates with project; el debug attaches debugger; --output json|junit for CI - 52 new tests in el-test covering discovery, graph seeding, assertion evaluation, pass/fail/error/skip, report generation, JUnit XML - Example: examples/hello-project/src/tests.el — 6 unit tests pass, 1 e2e test correctly skipped without ENGRAM_URL
This commit is contained in:
@@ -84,6 +84,38 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like `expect_ident` but also accepts keywords as bare names.
|
||||
/// Used in contexts like seed field names where `type:` must work.
|
||||
fn expect_ident_or_keyword(&mut self) -> Result<(String, Span), ParseError> {
|
||||
let span = self.peek_span();
|
||||
let name = match self.peek().clone() {
|
||||
Token::Ident(name) => name,
|
||||
// Accept any keyword as an identifier in seed field position
|
||||
Token::Type => "type".to_string(),
|
||||
Token::Fn => "fn".to_string(),
|
||||
Token::Let => "let".to_string(),
|
||||
Token::Enum => "enum".to_string(),
|
||||
Token::Match => "match".to_string(),
|
||||
Token::Return => "return".to_string(),
|
||||
Token::Activate => "activate".to_string(),
|
||||
Token::Where => "where".to_string(),
|
||||
Token::Sealed => "sealed".to_string(),
|
||||
Token::If => "if".to_string(),
|
||||
Token::Else => "else".to_string(),
|
||||
Token::For => "for".to_string(),
|
||||
Token::In => "in".to_string(),
|
||||
Token::Seed => "seed".to_string(),
|
||||
Token::Assert => "assert".to_string(),
|
||||
Token::Target => "target".to_string(),
|
||||
tok => return Err(ParseError::new(
|
||||
ParseErrorKind::ExpectedIdent(tok.to_string()),
|
||||
span,
|
||||
)),
|
||||
};
|
||||
self.advance();
|
||||
Ok((name, span))
|
||||
}
|
||||
|
||||
fn eat(&mut self, tok: &Token) -> bool {
|
||||
if self.peek() == tok {
|
||||
self.advance();
|
||||
@@ -178,7 +210,7 @@ impl Parser {
|
||||
let mut tier: Option<String> = None;
|
||||
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
let (field_name, _) = self.expect_ident_or_keyword()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
match field_name.as_str() {
|
||||
"type" => {
|
||||
@@ -221,7 +253,7 @@ impl Parser {
|
||||
let mut weight: f32 = 1.0;
|
||||
|
||||
while !matches!(self.peek(), Token::RBrace | Token::Eof) {
|
||||
let (field_name, _) = self.expect_ident()?;
|
||||
let (field_name, _) = self.expect_ident_or_keyword()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
match field_name.as_str() {
|
||||
"from" => {
|
||||
|
||||
Reference in New Issue
Block a user