From d15322d568c670cb57d2852d9d34d3af7bc44cad Mon Sep 17 00:00:00 2001 From: liangbowen Date: Wed, 13 Sep 2023 17:41:27 +0800 Subject: [PATCH] [KYUUBI #5275] [DOC] Improve and fix comparation and regeneration for golden files ### _Why are the changes needed?_ - Extract common assertion method for verifying file contents - Ensure integrity of the file by comparing the line count - Correct the script name for Spark engine KDF doc generation from `gen_kdf.sh` to `gen_spark_kdf_docs.sh` - Add `gen_hive_kdf_docs.sh` script for Hive engine KDF doc generation - Fix incorrect hints for Ranger spec file generation - shows the line number of the incorrect file content - Streamingly read file content by line with buffered support - Regeneration hints: image ### _How was this patch tested?_ - [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible - [ ] Add screenshots for manual tests if appropriate - [x] [Run test](https://kyuubi.readthedocs.io/en/master/contributing/code/testing.html#running-tests) locally before make a pull request ### _Was this patch authored or co-authored using generative AI tooling?_ No. Closes #5275 from bowenliang123/doc-regen-hint. Closes #5275 9af97ab86 [Bowen Liang] implicit source position 07020c74d [liangbowen] assertFileContent Lead-authored-by: liangbowen Co-authored-by: Bowen Liang Signed-off-by: Bowen Liang --- dev/gen/gen_hive_kdf_docs.sh | 26 ++++++++++ dev/gen/{gen_kdf.sh => gen_spark_kdf_docs.sh} | 2 +- .../authz/gen/PolicyJsonFileGenerator.scala | 13 +++-- .../authz/gen/JsonSpecFileGenerator.scala | 14 +++-- .../connector/tpcds/TPCDSQuerySuite.scala | 3 -- .../spark/connector/tpch/TPCHQuerySuite.scala | 3 -- .../hive/udf/KyuubiDefinedFunctionSuite.scala | 17 +++--- .../udf/KyuubiDefinedFunctionSuite.scala | 17 +++--- .../org/apache/kyuubi/MarkdownUtils.scala | 34 ------------ .../config/AllKyuubiConfiguration.scala | 7 ++- .../tpcds/OutputSchemaTPCDSSuite.scala | 16 +++--- .../apache/kyuubi/util/AssertionUtils.scala | 52 ++++++++++++++++++- .../apache/kyuubi/util/GoldenFileUtils.scala | 51 ++++++++++++++++++ 13 files changed, 165 insertions(+), 90 deletions(-) create mode 100755 dev/gen/gen_hive_kdf_docs.sh rename dev/gen/{gen_kdf.sh => gen_spark_kdf_docs.sh} (95%) create mode 100644 kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala diff --git a/dev/gen/gen_hive_kdf_docs.sh b/dev/gen/gen_hive_kdf_docs.sh new file mode 100755 index 000000000..b670dc3c5 --- /dev/null +++ b/dev/gen/gen_hive_kdf_docs.sh @@ -0,0 +1,26 @@ +#!/bin/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. +# + +# Golden result file: +# docs/extensions/engines/hive/functions.md + +KYUUBI_UPDATE="${KYUUBI_UPDATE:-1}" \ +build/mvn clean test \ + -pl externals/kyuubi-hive-sql-engine -am \ + -Pflink-provided,spark-provided,hive-provided \ + -DwildcardSuites=org.apache.kyuubi.engine.hive.udf.KyuubiDefinedFunctionSuite diff --git a/dev/gen/gen_kdf.sh b/dev/gen/gen_spark_kdf_docs.sh similarity index 95% rename from dev/gen/gen_kdf.sh rename to dev/gen/gen_spark_kdf_docs.sh index a10a31afc..ac13082e3 100755 --- a/dev/gen/gen_kdf.sh +++ b/dev/gen/gen_spark_kdf_docs.sh @@ -17,7 +17,7 @@ # # Golden result file: -# docs/sql/functions.md +# docs/extensions/engines/spark/functions.md KYUUBI_UPDATE="${KYUUBI_UPDATE:-1}" \ build/mvn clean test \ diff --git a/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala b/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala index 2686cba20..7faddd0c7 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/gen/scala/org/apache/kyuubi/plugin/spark/authz/gen/PolicyJsonFileGenerator.scala @@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.scala.DefaultScalaModule -import org.apache.commons.io.FileUtils import org.apache.ranger.plugin.model.RangerPolicy import org.scalatest.funsuite.AnyFunSuite @@ -37,6 +36,7 @@ import org.apache.kyuubi.plugin.spark.authz.gen.KRangerPolicyItemAccess.allowTyp import org.apache.kyuubi.plugin.spark.authz.gen.KRangerPolicyResource._ import org.apache.kyuubi.plugin.spark.authz.gen.RangerAccessType._ import org.apache.kyuubi.plugin.spark.authz.gen.RangerClassConversions._ +import org.apache.kyuubi.util.AssertionUtils._ /** * Generates the policy file to test/main/resources dir. @@ -77,12 +77,11 @@ class PolicyJsonFileGenerator extends AnyFunSuite { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) } else { - val existedFileContent = - FileUtils.readFileToString(policyFilePath.toFile, StandardCharsets.UTF_8) - withClue("Please regenerate the ranger policy file by running" - + " `dev/gen/gen_ranger_policy_json.sh`") { - assert(generatedStr.equals(existedFileContent)) - } + assertFileContent( + policyFilePath, + Seq(generatedStr), + "dev/gen/gen_ranger_policy_json.sh", + splitFirstExpectedLine = true) } } diff --git a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala index b10904c1e..855e25e87 100644 --- a/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala +++ b/extensions/spark/kyuubi-spark-authz/src/test/scala/org/apache/kyuubi/plugin/spark/authz/gen/JsonSpecFileGenerator.scala @@ -20,11 +20,11 @@ package org.apache.kyuubi.plugin.spark.authz.gen import java.nio.charset.StandardCharsets import java.nio.file.{Files, Paths, StandardOpenOption} -import org.apache.commons.io.FileUtils //scalastyle:off import org.scalatest.funsuite.AnyFunSuite import org.apache.kyuubi.plugin.spark.authz.serde.{mapper, CommandSpec} +import org.apache.kyuubi.util.AssertionUtils._ /** * Generates the default command specs to src/main/resources dir. @@ -39,7 +39,6 @@ import org.apache.kyuubi.plugin.spark.authz.serde.{mapper, CommandSpec} * dev/gen/gen_ranger_spec_json.sh * }}} */ - class JsonSpecFileGenerator extends AnyFunSuite { // scalastyle:on test("check spec json files") { @@ -70,12 +69,11 @@ class JsonSpecFileGenerator extends AnyFunSuite { StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) } else { - val existedFileContent = - FileUtils.readFileToString(filePath.toFile, StandardCharsets.UTF_8) - withClue(s"Check $filename failed. Please regenerate the ranger policy file by running" - + " `dev/gen/gen_ranger_spec_json.sh`.") { - assert(generatedStr.equals(existedFileContent)) - } + assertFileContent( + filePath, + Seq(generatedStr), + "dev/gen/gen_ranger_spec_json.sh", + splitFirstExpectedLine = true) } } } diff --git a/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSQuerySuite.scala b/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSQuerySuite.scala index d566e12a4..c99d7beca 100644 --- a/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSQuerySuite.scala +++ b/extensions/spark/kyuubi-spark-connector-tpcds/src/test/scala/org/apache/kyuubi/spark/connector/tpcds/TPCDSQuerySuite.scala @@ -28,7 +28,6 @@ import org.apache.kyuubi.{KyuubiFunSuite, Utils} import org.apache.kyuubi.spark.connector.common.GoldenFileUtils._ import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSession -// scalastyle:off line.size.limit /** * To run this test suite: * {{{ @@ -40,8 +39,6 @@ import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSessi * dev/gen/gen_tpcds_queries.sh * }}} */ -// scalastyle:on line.size.limit - @Slow class TPCDSQuerySuite extends KyuubiFunSuite { diff --git a/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala b/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala index ce119f806..a409a5fe9 100644 --- a/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala +++ b/extensions/spark/kyuubi-spark-connector-tpch/src/test/scala/org/apache/kyuubi/spark/connector/tpch/TPCHQuerySuite.scala @@ -28,7 +28,6 @@ import org.apache.kyuubi.{KyuubiFunSuite, Utils} import org.apache.kyuubi.spark.connector.common.GoldenFileUtils._ import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSession -// scalastyle:off line.size.limit /** * To run this test suite: * {{{ @@ -40,8 +39,6 @@ import org.apache.kyuubi.spark.connector.common.LocalSparkSession.withSparkSessi * dev/gen/gen_tpcdh_queries.sh * }}} */ -// scalastyle:on line.size.limit - @Slow class TPCHQuerySuite extends KyuubiFunSuite { diff --git a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/udf/KyuubiDefinedFunctionSuite.scala b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/udf/KyuubiDefinedFunctionSuite.scala index f76eefe84..08cb143e0 100644 --- a/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/udf/KyuubiDefinedFunctionSuite.scala +++ b/externals/kyuubi-hive-sql-engine/src/test/scala/org/apache/kyuubi/engine/hive/udf/KyuubiDefinedFunctionSuite.scala @@ -19,24 +19,23 @@ package org.apache.kyuubi.engine.hive.udf import java.nio.file.Paths -import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, MarkdownUtils, Utils} +import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, Utils} +import org.apache.kyuubi.util.GoldenFileUtils._ -// scalastyle:off line.size.limit /** * End-to-end test cases for configuration doc file - * The golden result file is "docs/sql/functions.md". + * The golden result file is "docs/extensions/engines/hive/functions.md". * * To run the entire test suite: * {{{ - * build/mvn clean test -pl externals/kyuubi-hive-sql-engine -am -Pflink-provided,spark-provided,hive-provided -DwildcardSuites=org.apache.kyuubi.engine.hive.udf.KyuubiDefinedFunctionSuite + * KYUUBI_UPDATE=0 dev/gen/gen_hive_kdf_docs.sh * }}} * * To re-generate golden files for entire suite, run: * {{{ - * KYUUBI_UPDATE=1 build/mvn clean test -pl externals/kyuubi-hive-sql-engine -am -Pflink-provided,spark-provided,hive-provided -DwildcardSuites=org.apache.kyuubi.engine.hive.udf.KyuubiDefinedFunctionSuite + * dev/gen/gen_hive_kdf_docs.sh * }}} */ -// scalastyle:on line.size.limit class KyuubiDefinedFunctionSuite extends KyuubiFunSuite { private val kyuubiHome: String = Utils.getCodeSourceLocation(getClass) @@ -60,10 +59,6 @@ class KyuubiDefinedFunctionSuite extends KyuubiFunSuite { builder += s"${func.name} | ${func.description} | ${func.returnType} | ${func.since}" } - MarkdownUtils.verifyOutput( - markdown, - builder, - getClass.getCanonicalName, - "externals/kyuubi-hive-sql-engine") + verifyOrRegenerateGoldenFile(markdown, builder.toMarkdown, "dev/gen/gen_hive_kdf_docs.sh") } } diff --git a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/udf/KyuubiDefinedFunctionSuite.scala b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/udf/KyuubiDefinedFunctionSuite.scala index 7387a7e9b..7a3f8c940 100644 --- a/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/udf/KyuubiDefinedFunctionSuite.scala +++ b/externals/kyuubi-spark-sql-engine/src/test/scala/org/apache/kyuubi/engine/spark/udf/KyuubiDefinedFunctionSuite.scala @@ -19,24 +19,23 @@ package org.apache.kyuubi.engine.spark.udf import java.nio.file.Paths -import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, MarkdownUtils, Utils} +import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, Utils} +import org.apache.kyuubi.util.GoldenFileUtils._ -// scalastyle:off line.size.limit /** * End-to-end test cases for configuration doc file - * The golden result file is "docs/sql/functions.md". + * The golden result file is "docs/extensions/engines/spark/functions.md". * * To run the entire test suite: * {{{ - * KYUUBI_UPDATE=0 dev/gen/gen_kdf.sh + * KYUUBI_UPDATE=0 dev/gen/gen_spark_kdf_docs.sh * }}} * * To re-generate golden files for entire suite, run: * {{{ - * dev/gen/gen_kdf.sh + * dev/gen/gen_spark_kdf_docs.sh * }}} */ -// scalastyle:on line.size.limit class KyuubiDefinedFunctionSuite extends KyuubiFunSuite { private val kyuubiHome: String = Utils.getCodeSourceLocation(getClass) @@ -60,10 +59,6 @@ class KyuubiDefinedFunctionSuite extends KyuubiFunSuite { builder += s"${func.name} | ${func.description} | ${func.returnType} | ${func.since}" } - MarkdownUtils.verifyOutput( - markdown, - builder, - getClass.getCanonicalName, - "externals/kyuubi-spark-sql-engine") + verifyOrRegenerateGoldenFile(markdown, builder.toMarkdown, "dev/gen/gen_spark_kdf_docs.sh") } } diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala index 2ac5e8c69..4dbe6ea67 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/MarkdownUtils.scala @@ -17,10 +17,6 @@ package org.apache.kyuubi -import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Path, StandardOpenOption} - -import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer import com.vladsch.flexmark.formatter.Formatter @@ -28,36 +24,6 @@ import com.vladsch.flexmark.parser.{Parser, ParserEmulationProfile, PegdownExten import com.vladsch.flexmark.profile.pegdown.PegdownOptionsAdapter import com.vladsch.flexmark.util.data.{MutableDataHolder, MutableDataSet} import com.vladsch.flexmark.util.sequence.SequenceUtils.EOL -import org.scalatest.Assertions.{assertResult, withClue} - -object MarkdownUtils { - - def verifyOutput( - markdown: Path, - newOutput: MarkdownBuilder, - agent: String, - module: String): Unit = { - val formatted = newOutput.toMarkdown - if (System.getenv("KYUUBI_UPDATE") == "1") { - Files.write( - markdown, - formatted.asJava, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING) - } else { - Files.readAllLines(markdown, StandardCharsets.UTF_8).asScala.toStream - .zipWithIndex.zip(formatted).foreach { case ((lineInFile, lineIndex), formattedLine) => - withClue( - s""" The markdown file ($markdown:${lineIndex + 1}) is out of date. - | Please update doc with KYUUBI_UPDATE=1 build/mvn clean test - | -pl $module -am -Pflink-provided,spark-provided,hive-provided - | -Dtest=none -DwildcardSuites=$agent \n""".stripMargin) { - assertResult(formattedLine)(lineInFile) - } - } - } - } -} class MarkdownBuilder { private val buffer = new ListBuffer[String] diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala index 0a5329988..f53fb3a61 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/config/AllKyuubiConfiguration.scala @@ -21,14 +21,14 @@ import java.nio.file.Paths import scala.collection.JavaConverters._ -import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, MarkdownUtils, Utils} +import org.apache.kyuubi.{KyuubiFunSuite, MarkdownBuilder, Utils} import org.apache.kyuubi.ctl.CtlConf import org.apache.kyuubi.ha.HighAvailabilityConf import org.apache.kyuubi.metrics.MetricsConf import org.apache.kyuubi.server.metadata.jdbc.JDBCMetadataStoreConf +import org.apache.kyuubi.util.GoldenFileUtils._ import org.apache.kyuubi.zookeeper.ZookeeperConf -// scalastyle:off line.size.limit /** * End-to-end test cases for configuration doc file * The golden result file is "docs/configuration/settings.md". @@ -43,7 +43,6 @@ import org.apache.kyuubi.zookeeper.ZookeeperConf * dev/gen/gen_all_config_docs.sh * }}} */ -// scalastyle:on line.size.limit class AllKyuubiConfiguration extends KyuubiFunSuite { private val kyuubiHome: String = Utils.getCodeSourceLocation(getClass).split("kyuubi-server")(0) private val markdown = Paths.get(kyuubiHome, "docs", "configuration", "settings.md") @@ -229,6 +228,6 @@ class AllKyuubiConfiguration extends KyuubiFunSuite { | executor and obey the Spark AQE behavior of Kyuubi system default. On the other hand, | for those users who do not have custom configurations will use system defaults.""" - MarkdownUtils.verifyOutput(markdown, builder, getClass.getCanonicalName, "kyuubi-server") + verifyOrRegenerateGoldenFile(markdown, builder.toMarkdown, "dev/gen/gen_all_config_docs.sh") } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/tpcds/OutputSchemaTPCDSSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/tpcds/OutputSchemaTPCDSSuite.scala index d17623fcf..9505c4a3b 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/tpcds/OutputSchemaTPCDSSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/tpcds/OutputSchemaTPCDSSuite.scala @@ -26,8 +26,9 @@ import org.apache.kyuubi.{DeltaSuiteMixin, WithKyuubiServer} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.server.mysql.MySQLJDBCTestHelper import org.apache.kyuubi.tags.DeltaTest +import org.apache.kyuubi.util.AssertionUtils._ +import org.apache.kyuubi.util.GoldenFileUtils._ -// scalastyle:off line.size.limit /** * To run this test suite: * {{{ @@ -39,7 +40,6 @@ import org.apache.kyuubi.tags.DeltaTest * dev/gen/gen_tpcds_output_schema.sh * }}} */ -// scalastyle:on line.size.limit @Slow @DeltaTest class OutputSchemaTPCDSSuite extends WithKyuubiServer @@ -74,7 +74,6 @@ class OutputSchemaTPCDSSuite extends WithKyuubiServer super.afterAll() } - private val regenerateGoldenFiles = sys.env.get("KYUUBI_UPDATE").contains("1") protected val baseResourcePath: Path = Paths.get("src", "test", "resources") private def fileToString(file: Path): String = { @@ -89,12 +88,15 @@ class OutputSchemaTPCDSSuite extends WithKyuubiServer val columnTypes = (1 to result.getMetaData.getColumnCount).map { i => s"${result.getMetaData.getColumnName(i)}:${result.getMetaData.getColumnTypeName(i)}" }.mkString("struct<", ",", ">\n") - if (regenerateGoldenFiles) { + if (isRegenerateGoldenFiles) { Files.write(goldenFile, columnTypes.getBytes()) + } else { + assertFileContent( + goldenFile, + Seq(columnTypes), + "dev/gen/gen_tpcds_output_schema.sh", + splitFirstExpectedLine = true) } - - val expected = fileToString(goldenFile) - assert(columnTypes === expected) } finally { result.close() } diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala index e12eb1bd4..2e9a0d3fe 100644 --- a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/AssertionUtils.scala @@ -16,11 +16,15 @@ */ package org.apache.kyuubi.util +import java.nio.charset.StandardCharsets +import java.nio.file.Path import java.util.Locale +import scala.collection.Traversable +import scala.io.Source import scala.reflect.ClassTag -import org.scalactic.source +import org.scalactic.{source, Prettifier} import org.scalatest.Assertions._ object AssertionUtils { @@ -54,6 +58,52 @@ object AssertionUtils { } } + /** + * Assert the file content is equal to the expected lines. + * If not, throws assertion error with the given regeneration hint. + * @param expectedLines expected lines + * @param path source file path + * @param regenScript regeneration script + * @param splitFirstExpectedLine whether to split the first expected line + * into multiple lines by EOL + */ + def assertFileContent( + path: Path, + expectedLines: Traversable[String], + regenScript: String, + splitFirstExpectedLine: Boolean = false)(implicit + prettifier: Prettifier, + pos: source.Position): Unit = { + val fileSource = Source.fromFile(path.toUri, StandardCharsets.UTF_8.name()) + try { + def expectedLinesIter = if (splitFirstExpectedLine) { + Source.fromString(expectedLines.head).getLines() + } else { + expectedLines.toIterator + } + val fileLinesIter = fileSource.getLines() + val regenerationHint = s"The file ($path) is out of date. " + { + if (regenScript != null && regenScript.nonEmpty) { + s" Please regenerate it by running `${regenScript.stripMargin}`. " + } else "" + } + var fileLineCount = 0 + fileLinesIter.zipWithIndex.zip(expectedLinesIter) + .foreach { case ((lineInFile, lineIndex), expectedLine) => + val lineNum = lineIndex + 1 + withClue(s"Line $lineNum is not expected. $regenerationHint") { + assertResult(expectedLine)(lineInFile)(prettifier, pos) + } + fileLineCount = Math.max(lineNum, fileLineCount) + } + withClue(s"Line number is not expected. $regenerationHint") { + assertResult(expectedLinesIter.size)(fileLineCount)(prettifier, pos) + } + } finally { + fileSource.close() + } + } + /** * Asserts that the given function throws an exception of the given type * and with the exception message equals to expected string diff --git a/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala new file mode 100644 index 000000000..e9927f7e2 --- /dev/null +++ b/kyuubi-util-scala/src/test/scala/org/apache/kyuubi/util/GoldenFileUtils.scala @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package org.apache.kyuubi.util + +import java.nio.file.{Files, Path, StandardOpenOption} + +import scala.collection.JavaConverters._ + +import org.apache.kyuubi.util.AssertionUtils._ + +object GoldenFileUtils { + def isRegenerateGoldenFiles: Boolean = sys.env.get("KYUUBI_UPDATE").contains("1") + + /** + * Verify the golden file content when KYUUBI_UPDATE env is not equals to 1, + * or regenerate the golden file content when KYUUBI_UPDATE env is equals to 1. + * + * @param path the path of file + * @param lines the expected lines for validation or regeneration + * @param regenScript the script for regeneration, used for hints when verification failed + */ + def verifyOrRegenerateGoldenFile( + path: Path, + lines: Iterable[String], + regenScript: String): Unit = { + if (isRegenerateGoldenFiles) { + Files.write( + path, + lines.asJava, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING) + } else { + assertFileContent(path, lines, regenScript) + } + } +}