diff --git a/src/main/java/org/eolang/lints/LtTestNotVerb.java b/src/main/java/org/eolang/lints/LtTestNotVerb.java index a5b49c0c5..4f916a99b 100644 --- a/src/main/java/org/eolang/lints/LtTestNotVerb.java +++ b/src/main/java/org/eolang/lints/LtTestNotVerb.java @@ -7,16 +7,8 @@ import com.github.lombrozo.xnav.Xnav; import com.jcabi.xml.XML; import java.io.IOException; -import java.util.Arrays; import java.util.Collection; -import java.util.Locale; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; -import opennlp.tools.postag.POSModel; -import opennlp.tools.postag.POSTaggerME; -import org.cactoos.io.InputStreamOf; -import org.cactoos.io.ResourceOf; /** * Lint that checks test object name is a verb in singular. @@ -28,43 +20,24 @@ final class LtTestNotVerb implements Lint { /** - * The pattern to split kebab case. + * Vocabulary for English name checks. */ - private static final Pattern KEBAB = Pattern.compile("-"); - - /** - * Part-Of-Speech tagger. - */ - private final POSTaggerME model; + private final Vocabulary vocabulary; /** * Ctor. * @throws IOException If fails */ LtTestNotVerb() throws IOException { - this( - new POSModel( - new InputStreamOf( - new ResourceOf("en-pos-perceptron.bin") - ) - ) - ); - } - - /** - * Ctor. - * @param mdl Part-Of-Speech model - */ - LtTestNotVerb(final POSModel mdl) { - this(new POSTaggerME(mdl)); + this(new Vocabulary()); } /** * Ctor. - * @param pos Part-Of-Speech tagger + * @param vocab Vocabulary to use for name checks */ - LtTestNotVerb(final POSTaggerME pos) { - this.model = pos; + LtTestNotVerb(final Vocabulary vocab) { + this.vocabulary = vocab; } @Override @@ -76,7 +49,7 @@ public String name() { public Collection defects(final XML xmir) throws IOException { return new Xnav(xmir.inner()) .path("/object//o[@name and starts-with(@name, '+')]") - .filter(object -> !this.isVerbInSingular(object)) + .filter(object -> !this.isVerb(object)) .map(LtTestNotVerb::verbDefect) .collect(Collectors.toList()); } @@ -87,22 +60,13 @@ public String motive() throws IOException { } /** - * Check if object name is verb in singular. + * Check if the test object name is a verb in singular. * @param object Object navigator * @return True if first word is a verb in singular form */ - private boolean isVerbInSingular(final Xnav object) { - return "VBZ".equals( - this.model.tag( - Stream.concat( - Stream.of("It"), - Arrays.stream( - LtTestNotVerb.KEBAB.split( - object.attribute("name").text().get().replace("+", "") - ) - ) - ).map(s -> s.toLowerCase(Locale.ROOT)).toArray(String[]::new) - )[1] + private boolean isVerb(final Xnav object) { + return this.vocabulary.isVerb( + object.attribute("name").text().get().replace("+", "") ); } diff --git a/src/main/java/org/eolang/lints/Vocabulary.java b/src/main/java/org/eolang/lints/Vocabulary.java new file mode 100644 index 000000000..78fd84d0a --- /dev/null +++ b/src/main/java/org/eolang/lints/Vocabulary.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import opennlp.tools.postag.POSModel; +import opennlp.tools.postag.POSTaggerME; +import org.cactoos.io.InputStreamOf; +import org.cactoos.io.ResourceOf; + +/** + * English vocabulary checks for EO object names. + * + *

Uses OpenNLP POS tagging to + * determine the grammatical role of words in a kebab-case name.

+ * + * @since 0.2.0 + */ +final class Vocabulary { + + /** + * Pattern to split kebab-case names. + */ + private static final Pattern KEBAB = Pattern.compile("-"); + + /** + * Part-Of-Speech tagger. + */ + private final POSTaggerME tagger; + + /** + * Ctor. + * @throws IOException If fails to load the POS model resource + */ + Vocabulary() throws IOException { + this( + new POSModel( + new InputStreamOf( + new ResourceOf("en-pos-perceptron.bin") + ) + ) + ); + } + + /** + * Ctor. + * @param mdl Part-Of-Speech model + */ + Vocabulary(final POSModel mdl) { + this(new POSTaggerME(mdl)); + } + + /** + * Ctor. + * @param pos Part-Of-Speech tagger + */ + Vocabulary(final POSTaggerME pos) { + this.tagger = pos; + } + + /** + * Check if the given kebab-case name starts with a verb in third-person singular. + * + *

The check uses the "It [verb]s" rule: "It generates-report" → the + * first word must be tagged {@code VBZ} (verb, 3rd-person singular present).

+ * + * @param name Kebab-case name without any leading {@code +} sigil + * @return True if the first word is a VBZ-tagged verb + */ + boolean isVerb(final String name) { + return "VBZ".equals( + this.tagger.tag( + Stream.concat( + Stream.of("It"), + Arrays.stream(Vocabulary.KEBAB.split(name)) + ).map(s -> s.toLowerCase(Locale.ROOT)).toArray(String[]::new) + )[1] + ); + } +} diff --git a/src/test/java/org/eolang/lints/LtTestNotVerbTest.java b/src/test/java/org/eolang/lints/LtTestNotVerbTest.java index 604a0a6bb..b88732a7c 100644 --- a/src/test/java/org/eolang/lints/LtTestNotVerbTest.java +++ b/src/test/java/org/eolang/lints/LtTestNotVerbTest.java @@ -17,68 +17,21 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link LtTestNotVerb}. * @since 0.0.22 - * @todo #872:60min Extract name-validation logic from LtTestNotVerb into a testable component. - * Currently {@link LtTestNotVerbTest} parses many EO programs that differ only in the - * object name being tested, making the tests slow and hard to maintain. The name-validation - * predicate (verb vs. non-verb check) should be extracted into its own class so it can be - * tested directly with plain strings — no EO parsing required. Once extracted, reduce - * {@link LtTestNotVerbTest} to a few representative end-to-end cases and add a dedicated - * unit test class for the new component. */ final class LtTestNotVerbTest { @ExtendWith(MayBeSlow.class) + @Execution(ExecutionMode.CONCURRENT) @ParameterizedTest - @ValueSource( - strings = { - "itIsTrue", - "testing", - "this-is-test", - "it-works", - "nothing-happened", - "something-is-wrong", - "will-fail-eventually", - "always-returns-true", - "was-lost-forever", - "nobody-knows-why", - "should-not-pass", - "once-upon-a-time", - "must-do-better", - "was-a-trap", - "dont-look-here", - "never-saw-it-coming", - "this-time-for-sure", - "there-it-goes", - "it-is-fine-probably", - "maybe-next-time", - "well-this-is-awkward", - "why-this-again", - "here-we-go-again", - "it-was-working-before", - "expected-the-unexpected", - "it-seems-fine", - "suddenly-works", - "too-late-now", - "could-not-care-less", - "it-just-works", - "dont-push-that-button", - "error-404-not-found", - "who-did-this", - "it-has-a-plan", - "will-never-finish", - "accidentally-passed", - "i-think-its-ok", - "chicken-as-expected", - "please-reboot", - "hope-it-works" - } - ) + @ValueSource(strings = {"it-works", "should-not-pass", "testing"}) void catchesBadName(final String name) throws IOException { MatcherAssert.assertThat( "Defects size doesn't match with expected", @@ -104,34 +57,9 @@ void catchesBadName(final String name) throws IOException { } @ExtendWith(MayBeSlow.class) + @Execution(ExecutionMode.CONCURRENT) @ParameterizedTest - @ValueSource( - strings = { - "generates-report", - "locks-branch", - "parses-dom", - "prints-data", - "runs", - "works-as-expected", - "breaks-hearts", - "crashes-again", - "forgets-everything", - "has-been-found", - "looks-fine-to-me", - "returns-something-strange", - "disappears-silently", - "follows-the-rules", - "finds-nothing-at-all", - "sounds-legit", - "sleeps-forever", - "makes-zero-sense", - "runs-in-circles", - "is-never-called", - "is-kind-of-slow", - "is-totally-broken", - "is-almost-correct" - } - ) + @ValueSource(strings = {"generates-report", "runs", "parses-dom"}) void allowsGoodNames(final String name) throws IOException { MatcherAssert.assertThat( "Defects are not empty, but they shouldn't be", diff --git a/src/test/java/org/eolang/lints/VocabularyTest.java b/src/test/java/org/eolang/lints/VocabularyTest.java new file mode 100644 index 000000000..05ce6c3e2 --- /dev/null +++ b/src/test/java/org/eolang/lints/VocabularyTest.java @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import java.io.IOException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Tests for {@link Vocabulary}. + * @since 0.2.0 + */ +final class VocabularyTest { + + @Execution(ExecutionMode.CONCURRENT) + @ParameterizedTest + @ValueSource( + strings = { + "itIsTrue", + "testing", + "this-is-test", + "it-works", + "nothing-happened", + "something-is-wrong", + "will-fail-eventually", + "always-returns-true", + "was-lost-forever", + "nobody-knows-why", + "should-not-pass", + "once-upon-a-time", + "must-do-better", + "was-a-trap", + "dont-look-here", + "never-saw-it-coming", + "this-time-for-sure", + "there-it-goes", + "it-is-fine-probably", + "maybe-next-time", + "well-this-is-awkward", + "why-this-again", + "here-we-go-again", + "it-was-working-before", + "expected-the-unexpected", + "it-seems-fine", + "suddenly-works", + "too-late-now", + "could-not-care-less", + "it-just-works", + "dont-push-that-button", + "error-404-not-found", + "who-did-this", + "it-has-a-plan", + "will-never-finish", + "accidentally-passed", + "i-think-its-ok", + "chicken-as-expected", + "please-reboot", + "hope-it-works" + } + ) + void detectsNonVerbName(final String name) throws IOException { + MatcherAssert.assertThat( + "Name should not be recognized as a verb in singular, but it was", + new Vocabulary().isVerb(name), + Matchers.is(false) + ); + } + + @Execution(ExecutionMode.CONCURRENT) + @ParameterizedTest + @ValueSource( + strings = { + "generates-report", + "locks-branch", + "parses-dom", + "prints-data", + "runs", + "works-as-expected", + "breaks-hearts", + "crashes-again", + "forgets-everything", + "has-been-found", + "looks-fine-to-me", + "returns-something-strange", + "disappears-silently", + "follows-the-rules", + "finds-nothing-at-all", + "sounds-legit", + "sleeps-forever", + "makes-zero-sense", + "runs-in-circles", + "is-never-called", + "is-kind-of-slow", + "is-totally-broken", + "is-almost-correct" + } + ) + void recognizesVerbName(final String name) throws IOException { + MatcherAssert.assertThat( + "Name should be recognized as a verb in singular, but it was not", + new Vocabulary().isVerb(name), + Matchers.is(true) + ); + } +}