/*
* This file is part of the Intuitive DSL project.
* Copyright (c) 2026 DBA Labs - Switzerland. All rights reserved.
*
* This program is dual-licensed under a commercial license and the AGPLv3.
* For commercial licensing, contact us at [email protected] or visit https://www.dbalabs.ch.
*
* AGPLv3 licensing:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 (19 November 2007).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
package ch.dbalabs.intuitivedsl.parser;
import ch.dbalabs.intuitivedsl.exception.DslSyntaxException;
import ch.dbalabs.intuitivedsl.grammar.GrammarCompiler;
import ch.dbalabs.intuitivedsl.grammar.GroupNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Sentinel tests for syntax diagnostics.
*
* These tests focus on the quality of the feedback given to the caller, not just on
* accept/reject behavior.
*/
class ErrorReportingSentinelTest {
private GrammarCompiler compiler;
private Lexer lexer;
private AstNavigator navigator;
@BeforeEach
void setUp() {
compiler = new GrammarCompiler();
lexer = new Lexer();
navigator = new AstNavigator();
}
@Test
void shouldMergeExpectedTokensAcrossAlternativesAtSameFailurePositionWithinSingleGrammar() {
GroupNode ast = compiler.compile("CREATE { USER username | ROLE role_name } ;");
assertThatThrownBy(() -> navigator.parse(
"CREATE GROUP admins ;",
lexer.tokenize("CREATE GROUP admins ;"),
ast,
m -> List.of()
)).isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("GROUP");
assertThat(ex.getExpectedPossibilities()).contains("USER", "ROLE");
});
}
@Test
void shouldReportSyntheticEofTokenWhenInputEndsTooEarly() {
GroupNode ast = compiler.compile("CREATE USER username ;");
String input = "CREATE USER";
assertThatThrownBy(() -> navigator.parse(
input,
lexer.tokenize(input),
ast,
m -> List.of()
)).isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().type()).isEqualTo(TokenType.EOF);
assertThat(ex.getUnexpectedToken().value()).isEqualTo("EOF");
assertThat(ex.getUnexpectedToken().position()).isEqualTo(input.length());
assertThat(ex.getExpectedPossibilities()).contains("<username>");
});
}
@Test
void shouldExposePreciseUnexpectedTokenPositionAndRichMessage() {
GroupNode ast = compiler.compile("DEPLOY service_name TO environment ;");
String input = "DEPLOY billing prod ;";
assertThatThrownBy(() -> navigator.parse(input, lexer.tokenize(input), ast, m -> List.of()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("prod");
assertThat(ex.getUnexpectedToken().position()).isEqualTo(input.indexOf("prod"));
assertThat(ex.getExpectedPossibilities()).contains("TO");
assertThat(ex.getRichMessage()).contains("Unexpected token 'prod'");
assertThat(ex.getRichMessage()).contains("Expected: { TO }");
});
}
@Test
void shouldReportMissingGrammarDelimiterWhenTrailingTokensAppearBeforeCommandEnd() {
GroupNode ast = compiler.compile("PING ;");
assertThatThrownBy(() -> navigator.parse(
"PING extra ;",
lexer.tokenize("PING extra ;"),
ast,
m -> List.of()
)).isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("extra");
assertThat(ex.getExpectedPossibilities()).contains("';'");
});
}
}