plugins { id 'java' id 'eclipse' id 'application' id 'maven-publish' id 'signing' id 'pmd' id 'jacoco' id 'info.solidsoft.pitest' version '1.19.0' id 'me.champeau.jmh' version '0.7.2' id 'org.owasp.dependencycheck' version '12.2.1' id 'org.cyclonedx.bom' version '3.2.4' id 'com.palantir.git-version' version '4.0.0' } group = 'org.egothor' version = gitVersion(prefix:'release@') def benchmarkReportsDirectory = layout.buildDirectory.dir('reports/jmh') def sbomReportsDirectory = layout.buildDirectory.dir('reports/sbom') def nvdApiKey = providers.gradleProperty('nvdApiKey') .orElse(providers.environmentVariable('NVD_API_KEY')) .orNull def dependencyCheckSuppressionFile = rootProject.file('dependency-suppression.xml') apply from: 'gradle/maven-pom.gradle' configurations { mockitoAgent } java { withSourcesJar() withJavadocJar() sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } tasks.withType(AbstractArchiveTask).configureEach { preserveFileTimestamps = false reproducibleFileOrder = true } jacoco { toolVersion = '0.8.14' } pmd { consoleOutput = true toolVersion = '7.20.0' sourceSets = [sourceSets.main] ruleSetFiles = files(rootProject.file(".ruleset")) } dependencyLocking { lockAllConfigurations() lockMode = LockMode.STRICT } dependencies { jmhImplementation sourceSets.main.output testImplementation platform(libs.junit.bom) testImplementation libs.junit.jupiter testRuntimeOnly libs.junit.platform.launcher testImplementation libs.mockito.core testImplementation libs.mockito.junit.jupiter testImplementation libs.jqwik mockitoAgent(libs.mockito.core) { transitive = false } } dependencyCheck { failBuildOnCVSS = 7.0 failOnError = true autoUpdate = true formats = ['HTML', 'JSON'] outputDirectory = layout.buildDirectory.dir('reports/dependency-check').get().asFile.absolutePath /* * Keep the scan focused on actual Java dependency inputs used by this project. * testRuntimeClasspath is included intentionally because the current external * dependency surface is primarily test-scoped. */ scanConfigurations = ['runtimeClasspath', 'testRuntimeClasspath', 'mockitoAgent'] skipTestGroups = false analyzers { experimentalEnabled = false centralEnabled = true } nvd { apiKey = nvdApiKey delay = nvdApiKey != null ? 3500 : 8000 validForHours = 4 } if (dependencyCheckSuppressionFile.exists()) { suppressionFile = dependencyCheckSuppressionFile.absolutePath failBuildOnUnusedSuppressionRule = true } } def cliIncludeTags = project.findProperty('includeTags')?.toString() ?: System.getProperty('includeTags') def cliExcludeTags = project.findProperty('excludeTags')?.toString() ?: System.getProperty('excludeTags') def splitTagExpression = { String tagsExpr -> if (tagsExpr == null || tagsExpr.isBlank()) { return [] } return tagsExpr.split(',') .collect { it.trim() } .findAll { it != null && !it.isBlank() } } tasks.withType(Test).configureEach { doFirst { jvmArgs "-javaagent:${configurations.mockitoAgent.singleFile}" } /* * Bundled dictionary integration tests compile and reload large real-world * stemming dictionaries, including large language resources such as es_es. * The default Gradle test executor heap is too small for this workload. */ minHeapSize = '1g' maxHeapSize = '4g' reports { junitXml.required = true html.required = true } } def configureJUnitPlatformTags = { Test task, String includeTagsExpr, String excludeTagsExpr -> task.useJUnitPlatform { final def includes = splitTagExpression(includeTagsExpr) final def excludes = splitTagExpression(excludeTagsExpr) if (!includes.isEmpty()) { includeTags(*includes.toArray(new String[0])) } if (!excludes.isEmpty()) { excludeTags(*excludes.toArray(new String[0])) } } } tasks.named('test', Test) { configureJUnitPlatformTags(it, cliIncludeTags, cliExcludeTags) finalizedBy(tasks.named('jacocoTestReport')) } def configureTaggedTestProfile = { String taskName, String includeTagsExpr, String excludeTagsExpr = null, String taskDescription = null, String testNameExcludePatterns = null -> tasks.register(taskName, Test) { group = 'verification' description = taskDescription configureJUnitPlatformTags(delegate as Test, includeTagsExpr, excludeTagsExpr) testClassesDirs = sourceSets.test.output.classesDirs classpath = sourceSets.test.runtimeClasspath dependsOn(tasks.named('compileTestJava')) doFirst { jvmArgs "-javaagent:${configurations.mockitoAgent.singleFile}" } if (testNameExcludePatterns != null && !testNameExcludePatterns.isBlank()) { filter { testNameExcludePatterns.split(',').each { String pattern -> final def trimmedPattern = pattern.trim() if (!trimmedPattern.isEmpty()) { excludeTestsMatching(trimmedPattern) } } } } minHeapSize = '1g' maxHeapSize = '4g' reports { junitXml.required = true html.required = true } } } configureTaggedTestProfile( 'ciSmoke', 'unit', 'slow', 'Fast feedback profile for unit tests with slow tests explicitly excluded.', 'org.egothor.stemmer.CompileIntegrationTest*' ) configureTaggedTestProfile( 'ciCore', 'unit,trie,frequency-trie,property', null, 'Focused profile for core trie behavior and trie-specific property checks.' ) configureTaggedTestProfile( 'ciIntegration', 'integration', 'slow', 'Integration pipeline profile (loader/parser/CLI/IO end-to-end flows) excluding slow integration paths.' ) configureTaggedTestProfile( 'ciSlow', 'slow', null, 'Targeted profile for all slow tests (large dictionaries, long-running corpus validation, and heavy integration checks).' ) configureTaggedTestProfile( 'ciCompat', 'compat,regression', null, 'Compatibility profile guarding persisted artifact and compatibility regressions.' ) configureTaggedTestProfile( 'ciRelease', null, 'slow', 'Release-profile validation of all non-slow tests.', 'org.egothor.stemmer.CompileIntegrationTest*,org.egothor.stemmer.StemmerPatchTrieLoaderTest$BundledDictionaryTests*' ) configureTaggedTestProfile( 'ciNightly', 'fuzz', null, 'Nightly robustness profile with fuzz testing emphasis.' ) tasks.register('ci') { group = 'verification' description = 'Runs the full enterprise CI profile set in sequence.' dependsOn(tasks.named('ciSmoke')) dependsOn(tasks.named('ciCore')) dependsOn(tasks.named('ciIntegration')) dependsOn(tasks.named('ciCompat')) } tasks.withType(Pmd).configureEach { reports { xml.required = true html.required = true } } tasks.named('jacocoTestReport', JacocoReport) { dependsOn(tasks.named('test')) classDirectories.setFrom( files(sourceSets.main.output).asFileTree.matching { exclude 'org/egothor/stemmer/StemmerKnowledgeExperiment*' exclude 'org/egothor/stemmer/DiacriticStripper*' } ) reports { xml.required = true csv.required = false html.required = true } } def registerJacocoProfileReport = { String reportTaskName, String sourceTaskName -> tasks.register(reportTaskName, JacocoReport) { group = 'verification' description = "Generates Jacoco report for ${sourceTaskName} execution." dependsOn(tasks.named(sourceTaskName)) classDirectories.setFrom( files(sourceSets.main.output).asFileTree.matching { exclude 'org/egothor/stemmer/StemmerKnowledgeExperiment*' exclude 'org/egothor/stemmer/DiacriticStripper*' } ) executionData.setFrom( fileTree(layout.buildDirectory.dir('jacoco')) { include "${sourceTaskName}.exec" } ) reports { xml.required = true csv.required = false html.required = true } } } registerJacocoProfileReport('jacocoCiReleaseReport', 'ciRelease') tasks.named('check') { dependsOn(tasks.named('jacocoTestReport')) // no-default, only on-demand: dependsOn(tasks.named('dependencyCheckAnalyze')) } allprojects { tasks.matching { it.name == 'cyclonedxDirectBom' }.configureEach { includeConfigs = ['runtimeClasspath', 'compileClasspath'] skipConfigs = ['testRuntimeClasspath', 'testCompileClasspath', 'jmh.*', 'mockitoAgent'] includeBomSerialNumber = true includeLicenseText = false includeMetadataResolution = true includeBuildSystem = true } } tasks.named('cyclonedxBom') { includeBomSerialNumber = true includeLicenseText = false includeBuildSystem = true jsonOutput.set(sbomReportsDirectory.map { it.file('radixor-sbom.json') }) xmlOutput.set(sbomReportsDirectory.map { it.file('radixor-sbom.xml') }) } pitest { pitestVersion = '1.22.1' junit5PluginVersion = '1.2.3' targetClasses = [ 'org.egothor.stemmer.*', 'org.egothor.stemmer.trie.*' ] targetTests = [ 'org.egothor.stemmer.*Test', 'org.egothor.stemmer.trie.*Test' ] excludedClasses = [ 'org.egothor.stemmer.Compile*', 'org.egothor.stemmer.StemmerPatchTrieLoader*', 'org.egothor.stemmer.StemmerKnowledgeExperiment*', 'org.egothor.stemmer.StemmerKnowledgeExperimentCli*' ] excludedTestClasses = [ 'org.egothor.stemmer.CompileIntegrationTest', 'org.egothor.stemmer.StemmerPatchTrieLoaderTest', 'org.egothor.stemmer.StemmerKnowledgeExperimentTest' ] outputFormats = ['XML', 'HTML'] timestampedReports = false exportLineCoverage = true failWhenNoMutations = true threads = Math.max(1, Runtime.runtime.availableProcessors().intdiv(2)) } application { mainClass = 'org.egothor.stemmer.Compile' applicationName = 'radixor' executableDir = 'bin' } tasks.register('stemmerKnowledgeExperiment', JavaExec) { group = 'application' description = 'Runs the stemmer knowledge evaluation experiment.' classpath = sourceSets.main.runtimeClasspath mainClass = 'org.egothor.stemmer.StemmerKnowledgeExperimentCli' } distributions { main { distributionBaseName = 'radixor' contents { from('README.md') { into '' } from('LICENSE') { into '' } from('LICENSE-stemmer-data') { into '' } from('docs') { into 'docs' include '**/*.md' } from(layout.buildDirectory.dir('generated/release-notes')) { into '' include 'CHANGELOG.md' } } } } tasks.named('startScripts') { applicationName = 'radixor' } tasks.named('distZip', Zip) { archiveBaseName = 'radixor' archiveClassifier = 'bin' } tasks.named('distTar') { enabled = false } jmh { jmhVersion = '1.37' warmupIterations = 3 iterations = 5 fork = 1 benchmarkMode = ['avgt'] timeUnit = 'ns' resultFormat = 'CSV' resultsFile = benchmarkReportsDirectory.map { it.file('jmh-results.csv').asFile }.get() humanOutputFile = benchmarkReportsDirectory.map { it.file('jmh-results.txt').asFile }.get() duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE } tasks.named('jmh') { group = 'verification' description = 'Runs JMH benchmarks for the Radixor algorithmic core and Snowball comparison suite.' } tasks.register('regressionArtifactGenerator', JavaExec) { group = 'verification' description = 'Generates deterministic compiled trie regression artifacts.' classpath = sourceSets.test.runtimeClasspath mainClass = 'org.egothor.stemmer.RegressionArtifactGenerator' if (project.hasProperty('regressionInput')) { args '--input', project.property('regressionInput').toString() } if (project.hasProperty('regressionOutput')) { args '--output', project.property('regressionOutput').toString() } if (project.hasProperty('regressionStoreOriginal')) { args '--store-original', project.property('regressionStoreOriginal').toString() } if (project.hasProperty('regressionReductionMode')) { args '--reduction-mode', project.property('regressionReductionMode').toString() } } tasks.register('printDependencyCheckNvdConfig') { doLast { System.out.println("NVD API key present: " + (nvdApiKey != null && !nvdApiKey.isBlank())) } } tasks.named('dependencyCheckAnalyze') { dependsOn(tasks.named('printDependencyCheckNvdConfig')) } javadoc { failOnError = false options.addStringOption('Xdoclint:all,-missing', '-quiet') options.addBooleanOption('html5', true) options.tags('apiNote:a:API Note:') options.tags('implSpec:a:Implementation Requirements:') options.tags('implNote:a:Implementation Note:') options.tags('param') options.tags('return') options.tags('throws') options.tags('since') options.tags('version') options.tags('serialData') options.tags('factory') options.tags('see') options.use = true options.author = true options.version = true options.windowTitle = 'Radixor - Egothor Stemmer' options.docTitle = 'Radixor - Egothor Stemmer API' options.overview = file('src/main/javadoc/overview.html') options.bottom = """ """ options.links('https://docs.oracle.com/en/java/javase/21/docs/api/') options.group('Core Stemming API', 'org.egothor.stemmer') options.group('Trie Infrastructure', 'org.egothor.stemmer.trie') source = sourceSets.main.allJava } apply from: 'gradle/snowball-benchmarks.gradle' gradle.taskGraph.whenReady { taskGraph -> def banner = """ \u001B[34m 8888888888 .d8888b. .d88888b. 88888888888 888 888 .d88888b. 8888888b. 888 d88P Y88b d88P" "Y88b 888 888 888 d88P" "Y88b 888 Y88b 888 888 888 888 888 888 888 888 888 888 888 888 8888888 888 888 888 888 8888888888 888 888 888 d88P 888 888 88888 888 888 888 888 888 888 888 8888888P" 888 888 888 888 888 888 888 888 888 888 888 T88b 888 Y88b d88P Y88b. .d88P 888 888 888 Y88b. .d88P 888 T88b 8888888888 "Y8888P88 "Y88888P" 888 888 888 "Y88888P" 888 T88b \u001B[36m Project : ${project.name} Version : ${project.version} \u001B[0m """ println banner }