From 12372530a8366ab35834b8a93c39775ab87564ed Mon Sep 17 00:00:00 2001 From: Steve Rowe Date: Thu, 15 Mar 2018 12:31:45 -0400 Subject: [PATCH] SOLR-10912: Add scripts for automatic patch validation --- build.xml | 4 +- dev-tools/README.txt | 15 +- .../test-patch/jenkins-precommit-script.sh | 90 ++++ .../lucene-solr-yetus-personality.sh | 399 ++++++++++++++++++ dev-tools/test-patch/test-patch.sh | 91 ++++ lucene/CHANGES.txt | 4 + 6 files changed, 594 insertions(+), 9 deletions(-) create mode 100644 dev-tools/test-patch/jenkins-precommit-script.sh create mode 100644 dev-tools/test-patch/lucene-solr-yetus-personality.sh create mode 100644 dev-tools/test-patch/test-patch.sh diff --git a/build.xml b/build.xml index 93415fba9a5..70f0cc783f5 100755 --- a/build.xml +++ b/build.xml @@ -113,7 +113,7 @@ - + @@ -124,7 +124,7 @@ - + diff --git a/dev-tools/README.txt b/dev-tools/README.txt index 404ce8ba76f..b0b7e4afcfe 100644 --- a/dev-tools/README.txt +++ b/dev-tools/README.txt @@ -6,10 +6,11 @@ as to the usefulness of the tools. Description of dev-tools/ contents: ./size-estimator-lucene-solr.xls -- Spreadsheet for estimating memory and disk usage in Lucene/Solr -./doap/ -- Lucene and Solr project descriptors in DOAP RDF format. -./eclipse/ -- Used to generate project descriptors for the Eclipse IDE. -./git/ -- Git documentation and resources. -./idea/ -- Used to generate project descriptors for IntelliJ's IDEA IDE. -./maven/ -- Mavenizes the Lucene/Solr packages -./netbeans/ -- Used to generate project descriptors for the Netbeans IDE. -./scripts/ -- Odds and ends for building releases, etc. +./doap/ -- Lucene and Solr project descriptors in DOAP RDF format. +./eclipse/ -- Used to generate project descriptors for the Eclipse IDE. +./git/ -- Git documentation and resources. +./idea/ -- Used to generate project descriptors for IntelliJ's IDEA IDE. +./maven/ -- Mavenizes the Lucene/Solr packages +./netbeans/ -- Used to generate project descriptors for the Netbeans IDE. +./scripts/ -- Odds and ends for building releases, etc. +./test-patch/ -- Scripts for automatically validating patches diff --git a/dev-tools/test-patch/jenkins-precommit-script.sh b/dev-tools/test-patch/jenkins-precommit-script.sh new file mode 100644 index 00000000000..155e333a857 --- /dev/null +++ b/dev-tools/test-patch/jenkins-precommit-script.sh @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ---------------------------------------------------------------------------------- +# +# This script is a copy of the one used by ASF Jenkins's PreCommit-LUCENE-Build job. +# +# See the script "test-patch.sh" in this directory for the script to use for +# local manual patch validation. +# +# For other examples of scripts used to invoke Yetus, see the configuration on the +# PreCommit jobs on ASF Jenkins: https://builds.apache.org/view/PreCommit+Builds/ +# +# ------------>8-------------------------->8-------------------------->8------------ + +#!/usr/bin/env bash + +# This is a modified copy of the script from Jenkins project "PreCommit-HADOOP-Build" + +YETUSDIR=${WORKSPACE}/yetus +TESTPATCHBIN=${YETUSDIR}/precommit/test-patch.sh +ARTIFACTS=${WORKSPACE}/out +BASEDIR=${WORKSPACE}/sourcedir +rm -rf "${ARTIFACTS}" +mkdir -p "${ARTIFACTS}" + +if [[ -d /sys/fs/cgroup/pids/user.slice ]]; then + pids=$(cat /sys/fs/cgroup/pids/user.slice/user-910.slice/pids.max) + + if [[ ${pids} -gt 13000 ]]; then + echo "passed: ${pids}" + PIDMAX=10000 + else + echo "failed: ${pids}" + PIDMAX=5500 + fi +else + systemctl status $$ 2>/dev/null + echo "passed? no limit on trusty?" + PIDMAX=10000 +fi + +# One-time operation: download and expand Yetus source release +# TODO: when upgrading the Yetus release, remove the old tarball +YETUS_RELEASE=0.7.0 +YETUS_TARBALL="yetus-${YETUS_RELEASE}.tar.gz" +if [[ ! -f "${YETUS_TARBALL}" || ! -d "$YETUSDIR}}" ]]; then + echo "Downloading Yetus ${YETUS_RELEASE}" + curl -L "https://api.github.com/repos/apache/yetus/tarball/rel/${YETUS_RELEASE}" -o "${YETUS_TARBALL}" + if [[ -d "${YETUSDIR}" ]]; then + rm -rf "${YETUSDIR}" + mkdir -p "${YETUSDIR}" + fi + gunzip -c "${YETUS_TARBALL}" | tar xpf - -C "${YETUSDIR}" --strip-components 1 +fi + + +YETUS_ARGS+=("--project=LUCENE") +YETUS_ARGS+=("--basedir=${BASEDIR}") +YETUS_ARGS+=("--patch-dir=${ARTIFACTS}") +YETUS_ARGS+=("--personality=${BASEDIR}/dev-tools/test-patch/lucene-solr-yetus-personality.sh") +YETUS_ARGS+=("--jira-user=lucenesolrqa") +YETUS_ARGS+=("--jira-password=$JIRA_PASSWORD") +YETUS_ARGS+=("--brief-report-file=${ARTIFACTS}/email-report.txt") +YETUS_ARGS+=("--console-report-file=${ARTIFACTS}/console-report.txt") +YETUS_ARGS+=("--html-report-file=${ARTIFACTS}/console-report.html") +YETUS_ARGS+=("--proclimit=${PIDMAX}") +YETUS_ARGS+=("--console-urls") +YETUS_ARGS+=("--debug") +YETUS_ARGS+=("--skip-dirs=dev-tools") +YETUS_ARGS+=("--bugcomments=jira") +YETUS_ARGS+=("--resetrepo") +YETUS_ARGS+=("--run-tests") +YETUS_ARGS+=("--contrib-guide=https://wiki.apache.org/lucene-java/HowToContribute#Contributing_your_work") +YETUS_ARGS+=("--jenkins") +YETUS_ARGS+=("LUCENE-${ISSUE_NUM}") + +/bin/bash ${TESTPATCHBIN} "${YETUS_ARGS[@]}" \ No newline at end of file diff --git a/dev-tools/test-patch/lucene-solr-yetus-personality.sh b/dev-tools/test-patch/lucene-solr-yetus-personality.sh new file mode 100644 index 00000000000..e5196398d87 --- /dev/null +++ b/dev-tools/test-patch/lucene-solr-yetus-personality.sh @@ -0,0 +1,399 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This is a Yetus precommit "personality" (aka customized configuration) for Lucene/Solr. +# +# See the Yetus precommit documentation at https://yetus.apache.org/documentation/0.7.0/ +# and especially https://yetus.apache.org/documentation/0.7.0/precommit-advanced/. +# See also the Yetus source code for other projects' personality examples at +# https://git-wip-us.apache.org/repos/asf?p=yetus.git;f=precommit/personality;a=tree;hb=HEAD +# +# To add a new validation method (aka "plugin"): +# 1) Add its name to the PLUGIN_LIST below +# 2) Invoke "add_test_type" with it below +# 3) Add a "_filefilter" function to decide whether the plugin needs to be run based on changed files +# 4) Add a "_rebuild" function to call out to ant to perform the validation method. +# See examples of the above-described function types ^^ below. + +PLUGIN_LIST="ant,testoutput,jira,compile,javac,junit,unit,test4tests,checkluceneversion" +PLUGIN_LIST+=",ratsources,checkforbiddenapis,checklicenses,validatesourcepatterns,validaterefguide" +personality_plugins "${PLUGIN_LIST}" + +add_test_type "checkluceneversion" +add_test_type "ratsources" +add_test_type "checkforbiddenapis" +add_test_type "checklicenses" +add_test_type "validatesourcepatterns" +add_test_type "validaterefguide" + +add_test_format "testoutput" + +## @description Globals specific to this personality +## @audience private +## @stability evolving +function personality_globals +{ + #shellcheck disable=SC2034 + PATCH_BRANCH_DEFAULT=master + #shellcheck disable=SC2034 + JIRA_ISSUE_RE='^(LUCENE|SOLR)-[0-9]+$' + #shellcheck disable=SC2034 + JIRA_STATUS_RE='Patch Available' + #shellcheck disable=SC2034 + GITHUB_REPO="apache/lucene-solr" + #shellcheck disable=SC2034 + BUILDTOOL=ant +} + +## @description Queue up modules for this personality +## @audience private +## @stability evolving +## @param repostatus +## @param testtype +function personality_modules +{ + local repostatus=$1 + local testtype=$2 + + local module + local extra + + local moduleType="submodules" + + yetus_debug "Personality (lucene-solr): ${repostatus} ${testtype}" + + clear_personality_queue + + case ${testtype} in + clean|distclean|validatesourcepatterns) + moduleType="top" + ;; + checkluceneversion) + moduleType="solr" + ;; + ratsources) + moduleType="submodules" + ;; + checkforbiddenapis) + moduleType="both" + ;; + checklicenses) + moduleType="mains" + ;; + validaterefguide) + moduleType="solr-ref-guide" + ;; + compile) + moduleType="submodules" + extra="compile-test" + ;; + junit|unit) + moduleType="submodules" + extra="test" + ;; + *) + ;; + esac + + case ${moduleType} in + submodules) + for module in "${CHANGED_MODULES[@]}"; do + if [[ ! "$module" =~ ^lucene/(licenses|site) ]]; then # blacklist lucene/ dirs that aren't modules + if [[ "$module" =~ ^(lucene/(analysis/[^/]+|[^/]+)) ]]; then + lucene_module=${BASH_REMATCH[0]} + personality_enqueue_module "${lucene_module}" "$extra" + elif [[ "$module" =~ ^solr/(core|solrj|test-framework|solr-ref-guide|contrib/[^.]+) ]]; then # whitelist solr/ modules + solr_module=${BASH_REMATCH[0]} + personality_enqueue_module "${solr_module}" "$extra" + fi + fi + done + ;; + lucene|solr) + personality_enqueue_module "${moduleType}" "$extra" + ;; + top) + personality_enqueue_module . "$extra" + ;; + mains) + personality_enqueue_module "lucene" "$extra" + personality_enqueue_module "solr" "$extra" + ;; + both) # solr, lucene, or both + # personality_enqueue_module KEEPS duplicates, so de-dupe first + local doSolr=0,doLucene=0 + for module in "${CHANGED_MODULES[@]}"; do + if [[ "$module" =~ ^solr/ ]]; then doSolr=1; fi + if [[ "$module" =~ ^lucene/ ]]; then doLucene=1; fi + done + if [[ $doLucene == 1 ]]; then + if [[ $doSolr == 1 ]]; then + personality_enqueue_module . "$extra" + else + personality_enqueue_module "lucene" "$extra" + fi + elif [[ $doSolr == 1 ]]; then + personality_enqueue_module "solr" "$extra" + fi + ;; + solr-ref-guide) + for module in "${CHANGED_MODULES[@]}"; do + if [[ "$module" =~ ^solr/solr-ref-guide ]]; then + personality_enqueue_module "solr/solr-ref-guide" "$extra" + fi + done + ;; + *) + ;; + esac +} + +## @description Add tests based upon personality needs +## @audience private +## @stability evolving +## @param filename +function personality_file_tests +{ + declare filename=$1 + + yetus_debug "Using Lucene/Solr-specific personality_file_tests" + + if [[ ${filename} =~ build\.xml$ || ${filename} =~ /src/(java|resources|test|test-files|tools) ]]; then + yetus_debug "tests/unit: ${filename}" + add_test compile + add_test javac + add_test unit + fi +} + +## @description hook to reroute junit folder to search test results based on the module +## @audience private +## @stability evolving +## @param module +## @param buildlogfile +function testoutput_process_tests +{ + # shellcheck disable=SC2034 + declare module=$1 + declare buildlogfile=$2 + if [[ "${module}" =~ ^lucene/analysis/ ]]; then + JUNIT_TEST_OUTPUT_DIR="../../build/${module#*/}" + elif [[ "${module}" =~ ^solr/contrib/extraction ]]; then + JUNIT_TEST_OUTPUT_DIR="../../build/contrib/solr-cell" + elif [[ "${module}" =~ ^solr/contrib/(.*) ]]; then + JUNIT_TEST_OUTPUT_DIR="../../build/contrib/solr-${BASH_REMATCH[1]}" + elif [[ "${module}" =~ ^(lucene|solr)/ ]]; then + JUNIT_TEST_OUTPUT_DIR="../build/${module#*/}" + fi + yetus_debug "Rerouting build dir for junit to ${JUNIT_TEST_OUTPUT_DIR}" +} + +## @description checkluceneversion file filter +## @audience private +## @stability evolving +## @param filename +function checkluceneversion_filefilter +{ + local filename=$1 + if [[ ${filename} =~ ^solr/(example|server/solr/configsets) ]]; then + yetus_debug "tests/checkluceneversion: ${filename}" + add_test checkluceneversion + fi +} + +## @description checkluceneversion test +## @audience private +## @stability evolving +## @param repostatus +function checkluceneversion_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "checkluceneversion" "check-example-lucene-match-version" "Check configsets' lucene version" +} + +## @description ratsources file filter +## @audience private +## @stability evolving +## @param filename +function ratsources_filefilter +{ + local filename=$1 + if [[ ${filename} =~ /src/|\.xml$ ]] ; then + yetus_debug "tests/ratsources: ${filename}" + add_test ratsources + fi +} + +## @description ratsources test +## @audience private +## @stability evolving +## @param repostatus +function ratsources_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "ratsources" "rat-sources" "Release audit (RAT)" +} + +## @description checkforbiddenapis file filter +## @audience private +## @stability evolving +## @param filename +function checkforbiddenapis_filefilter +{ + local filename=$1 + if [[ ${filename} =~ \.java$ ]] ; then + yetus_debug "tests/checkforbiddenapis: ${filename}" + add_test checkforbiddenapis + fi +} + +## @description checkforbiddenapis test +## @audience private +## @stability evolving +## @param repostatus +function checkforbiddenapis_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "checkforbiddenapis" "check-forbidden-apis" "Check forbidden APIs" +} + +## @description checklicenses file filter +## @audience private +## @stability evolving +## @param filename +function checklicenses_filefilter +{ + local filename=$1 + if [[ ${filename} =~ (lucene|solr)/licenses/|lucene/ivy-versions.properties$ ]]; then + yetus_debug "tests/checklicenses: ${filename}" + add_test checklicenses + fi +} + +## @description checklicenses test +## @audience private +## @stability evolving +## @param repostatus +function checklicenses_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "checklicenses" "check-licenses" "Check licenses" +} + +## @description validaterefguide file filter +## @audience private +## @stability evolving +## @param filename +function validaterefguide_filefilter +{ + local filename=$1 + if [[ ${filename} =~ solr/solr-ref-guide ]]; then + yetus_debug "tests/validaterefguide: ${filename}" + add_test validaterefguide + fi +} + +## @description validaterefguide test +## @audience private +## @stability evolving +## @param repostatus +function validaterefguide_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "validaterefguide" "bare-bones-html-validation" "Validate ref guide" +} + +## @description validatesourcepatterns file filter +## @audience private +## @stability evolving +## @param filename +function validatesourcepatterns_filefilter +{ + local filename=$1 + if [[ ${filename} =~ \.(java|jflex|py|pl|g4|jj|html|js|css|xml|xsl|vm|sh|cmd|bat|policy|properties|mdtext|groovy|template|adoc|json)$ ]]; then + yetus_debug "tests/validatesourcepatterns: ${filename}" + add_test validatesourcepatterns + fi +} + +## @description validatesourcepatterns test +## @audience private +## @stability evolving +## @param repostatus +function validatesourcepatterns_rebuild +{ + local repostatus=$1 + lucene_ant_command ${repostatus} "validatesourcepatterns" "validate-source-patterns" "Validate source patterns" +} + +function lucene_ant_command +{ + declare repostatus=$1 + declare testname=$2 + declare antcommand=$3 + declare title=$4 + + declare result=0 + declare i=0 + declare module + declare fn + declare result + + if [[ "${repostatus}" = branch ]]; then + return 0 + fi + + if ! verify_needed_test ${testname}; then + echo "${BUILDMODEMSG} does not need ${testname} testing." + return 0 + fi + + big_console_header "${title}" + personality_modules $repostatus $testname + until [[ ${i} -eq ${#MODULE[@]} ]]; do + if [[ ${MODULE_STATUS[${i}]} == -1 ]]; then + ((result=result+1)) + ((i=i+1)) + continue + fi + ANT_ARGS=${antcommand} + + start_clock + module=${MODULE[$i]} + fn=$(module_file_fragment "${module}") + logfilename="${repostatus}-${antcommand}-${fn}.txt"; + logfile="${PATCH_DIR}/${logfilename}" + buildtool_cwd "${i}" + echo_and_redirect "${logfile}" $(ant_executor) + + if [[ $? == 0 ]] ; then + module_status ${i} +1 "${logfilename}" "${title}" "${antcommand} passed" + else + module_status ${i} -1 "${logfilename}" "${title}" "${antcommand} failed" + ((result=result+1)) + fi + ((i=i+1)) + savestop=$(stop_clock) + MODULE_STATUS_TIMER[${i}]=${savestop} + done + ANT_ARGS="" + if [[ ${result} -gt 0 ]]; then + modules_messages ${repostatus} "${title}" false + return 1 + fi + modules_messages ${repostatus} "${title}" true + return 0 +} diff --git a/dev-tools/test-patch/test-patch.sh b/dev-tools/test-patch/test-patch.sh new file mode 100644 index 00000000000..3d7bb3879b3 --- /dev/null +++ b/dev-tools/test-patch/test-patch.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# Invoke Yetus locally to validate a patch against Lucene/Solr, and +# (optionally) post a validation report comment on the passed-in JIRA issue +# from which the patch was downloaded. +# +# NB 1: The Lucene/Solr Yetus personality currently performs the equivalent +# of "ant precommit" and "ant test" in modified modules; instead of using +# this script to test your changes, please consider invoking those targets +# directly. +# +# NB 2: The Jenkins job "PreCommit-Admin" automatically detects new patches +# posted to LUCENE and SOLR JIRA issues that are in the "Patch Available" +# state, and then queues the appropriate "PreCommit-LUCENE-Build" or +# "PreCommit-SOLR-Build" job pointing to the JIRA hosting the new patch. +# Those jobs perform the same checks as this script, and like this script, +# will post a comment on the JIRA issue. As a result, manual invocation +# (e.g. via this script) should ordinarily not be necessary. +# +# Environment variable ${YETUS_HOME} must point to the separately installed +# Yetus home directory, e.g. the "rel/0.7.0" tag checked out from a local +# Yetus Git repository. +# +# Environment variable ${PROJECT_DIR} must point to a local Lucene/Solr git +# workspace dir. +# +# The sole cmdline param can be a JIRA issue, a local patch file, +# or a URL to a patch file. +# +# If the cmdline param is a JIRA issue, the patch to download and validate +# will be the most recently uploaded patch on the issue. See the patch +# naming schema that Yetus recognizes: +# https://yetus.apache.org/documentation/in-progress/precommit-patchnames/ +# +# If the cmdline param is a JIRA issue and you provide JIRA user/password via +# environment variables ${JIRA_USER} and ${JIRA_PASSWORD}, a patch validation +# report will be posted as a comment on the JIRA issue. + +help () { + echo "Usage 1: [ JIRA_USER=xxx JIRA_PASSWORD=yyy ] PROJECT_DIR=/path/to/lucene-solr YETUS_HOME=/path/to/yetus $0 SOLR-12345" + echo "Usage 2: [ JIRA_USER=xxx JIRA_PASSWORD=yyy ] PROJECT_DIR=/path/to/lucene-solr YETUS_HOME=/path/to/yetus $0 LUCENE-12345" + echo "Usage 3: PROJECT_DIR=/path/to/lucene-solr YETUS_HOME=/path/to/yetus $0 ../local.patch" + echo "Usage 4: PROJECT_DIR=/path/to/lucene-solr YETUS_HOME=/path/to/yetus $0 http://example.com/remote.patch" +} + +if [[ -z "${PROJECT_DIR}" || -z "${YETUS_HOME}" || -z "${1}" || "${1}" =~ ^-(-?h(elp)?|\?)$ ]] ; then + help + exit 1 +fi + +PATCH_REF=${1} +TEST_PATCH_BIN="${YETUS_HOME}/precommit/test-patch.sh" +SCRIPT_DIR="$( cd "$( dirname "${0}" )" && pwd )" +declare -a YETUS_ARGS + +if [[ ${PATCH_REF} =~ ^(LUCENE|SOLR)- ]]; then + JIRA_PROJECT=${BASH_REMATCH[0]} + YETUS_ARGS+=("--project=${JIRA_PROJECT}") + + if [[ -n "${JIRA_USER}" ]] && [[ -n "${JIRA_PASSWORD}" ]] ; then + YETUS_ARGS+=("--jira-user=${JIRA_USER}") + YETUS_ARGS+=("--jira-password=${JIRA_PASSWORD}") + YETUS_ARGS+=("--bugcomments=jira") + fi +fi + +YETUS_ARGS+=("--basedir=${PROJECT_DIR}") +YETUS_ARGS+=("--personality=${SCRIPT_DIR}/lucene-solr-yetus-personality.sh") +YETUS_ARGS+=("--skip-dirs=dev-tools") +YETUS_ARGS+=("--resetrepo") +YETUS_ARGS+=("--run-tests") +YETUS_ARGS+=("--debug") +YETUS_ARGS+=("--robot") +YETUS_ARGS+=("${PATCH_REF}") + +/bin/bash ${TEST_PATCH_BIN} "${YETUS_ARGS[@]}" diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 6be005ddedc..b54b1ae9c55 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -103,6 +103,10 @@ New Features deleted documents around for later reuse. See "IW.softUpdateDocument(...)" for reference. (Simon Willnauer) +Other + +* SOLR-10912: Add automatic patch validation. (Mano Kovacs, Steve Rowe) + ======================= Lucene 7.3.0 ======================= API Changes