You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2020/05/10 16:50:19 UTC
[incubator-nlpcraft] 02/02: SQL test model refactoring. SQL
developer tests added, initial version.
This is an automated email from the ASF dual-hosted git repository.
sergeykamov pushed a commit to branch NLPCRAFT-30
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git
commit dc1157a203ffc3d14bbefd8b514be74533a9cd00
Author: Sergey Kamov <se...@apache.org>
AuthorDate: Sun May 10 19:50:08 2020 +0300
SQL test model refactoring. SQL developer tests added, initial version.
---
.../apache/nlpcraft/examples/sql/SqlModel.scala | 5 +-
.../nlpcraft/examples/sql/SqlModelTest.scala | 92 ++++++++++
.../nlpcraft/examples/sql/db/SqlBuilder.scala | 97 +++++-----
.../nlpcraft/examples/sql/db/sample_questions.txt | 3 -
.../apache/nlpcraft/examples/sql/sql_model.yaml | 2 +-
.../nlpcraft/examples/sql/NCSqlModelSpec.scala | 200 +++++++++++++++++++++
.../mgrs/nlp/enrichers/NCEnricherBaseSpec.scala | 10 +-
7 files changed, 355 insertions(+), 54 deletions(-)
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
index 5c79878..bce080a 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
@@ -145,7 +145,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
groupBy { case (col, _) ⇒ col }.
map { case (col, seq) ⇒ SqlInCondition(col, seq.map { case (_, value) ⇒ value})}.toSeq
- private def select0(
+ protected def select0(
ext: NCSqlExtractor,
tabs: Seq[NCToken],
cols: Seq[NCToken],
@@ -191,7 +191,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
}
}
-
+ // General case.
@NCIntent(
"intent=commonReport conv=true " +
"term(tabs)={groups @@ 'table'}[0,7] " +
@@ -232,6 +232,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
)
}
+ // Case with implicit sort.
@NCIntent(
"intent=customSortReport conv=true " +
"term(sort)={id == 'sort:best' || id == 'sort:worst'} " +
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
index feb9892..f6294f1 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
@@ -589,6 +589,98 @@ class SqlModelTest {
|LIMIT
| 25
""".stripMargin
+ ),
+ Case(
+ Seq(
+ "What are the best performing products for the last quarter?"
+ ),
+ """SELECT
+ | orders.freight,
+ | orders.order_date,
+ | orders.order_id,
+ | orders.required_date,
+ | products.product_id,
+ | products.product_name,
+ | products.quantity_per_unit,
+ | categories.category_id,
+ | categories.category_name,
+ | categories.description,
+ | customers.customer_id,
+ | customers.company_name,
+ | customers.contact_name,
+ | employees.employee_id,
+ | employees.last_name,
+ | employees.first_name,
+ | order_details.unit_price,
+ | order_details.quantity,
+ | order_details.discount,
+ | shippers.shipper_id,
+ | shippers.company_name,
+ | shippers.phone,
+ | suppliers.supplier_id,
+ | suppliers.company_name,
+ | suppliers.contact_name
+ |FROM
+ | order_details
+ | INNER JOIN orders ON order_details.order_id = orders.order_id
+ | INNER JOIN products ON order_details.product_id = products.product_id
+ | LEFT JOIN suppliers ON products.supplier_id = suppliers.supplier_id
+ | LEFT JOIN categories ON products.category_id = categories.category_id
+ | LEFT JOIN customers ON orders.customer_id = customers.customer_id
+ | LEFT JOIN shippers ON orders.ship_via = shippers.shipper_id
+ | LEFT JOIN employees ON orders.employee_id = employees.employee_id
+ |WHERE
+ | orders.order_date >= ?
+ | AND orders.order_date <= ?
+ |ORDER BY
+ | orders.freight DESC
+ |LIMIT
+ | 1000
+ """.stripMargin
+ ),
+ Case(
+ Seq(
+ "What are the least performing categories for the last quarter?"
+ ),
+ """SELECT
+ | orders.freight,
+ | orders.order_date,
+ | categories.category_id,
+ | categories.category_name,
+ | categories.description,
+ | orders.order_id,
+ | orders.required_date,
+ | customers.customer_id,
+ | customers.company_name,
+ | customers.contact_name,
+ | employees.employee_id,
+ | employees.last_name,
+ | employees.first_name,
+ | order_details.unit_price,
+ | order_details.quantity,
+ | order_details.discount,
+ | products.product_id,
+ | products.product_name,
+ | products.quantity_per_unit,
+ | shippers.shipper_id,
+ | shippers.company_name,
+ | shippers.phone
+ |FROM
+ | products
+ | LEFT JOIN categories ON products.category_id = categories.category_id
+ | INNER JOIN order_details ON order_details.product_id = products.product_id
+ | INNER JOIN orders ON order_details.order_id = orders.order_id
+ | LEFT JOIN customers ON orders.customer_id = customers.customer_id
+ | LEFT JOIN shippers ON orders.ship_via = shippers.shipper_id
+ | LEFT JOIN employees ON orders.employee_id = employees.employee_id
+ |WHERE
+ | orders.order_date >= ?
+ | AND orders.order_date <= ?
+ |ORDER BY
+ | orders.freight ASC
+ |LIMIT
+ | 1000
+ """.stripMargin
)
)
}
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
index 46970c3..181efb6 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
@@ -85,23 +85,15 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
private def sql(tabs: Seq[NCSqlTable]): String = {
val names = tabs.map(_.getTable)
- println("names-" + names.mkString("|"))
- println("joins=\n" + schemaJoins.map(p ⇒ s"${p.getFromTable} - ${p.getToTable}").mkString("\n"))
- println()
-
- val refs = names.
- flatMap(t ⇒ schemaJoins.filter(j ⇒ j.getFromTable == t && names.contains(j.getToTable))).
- sortBy(_.getFromTable).
- zipWithIndex.map { case (join, idx) ⇒
- }
-
names.size match {
case 0 ⇒ throw new AssertionError(s"Unexpected empty tables")
case 1 ⇒ names.head
case _ ⇒
+ val used = mutable.HashSet.empty[String]
+
val refs = names.
flatMap(t ⇒ schemaJoins.filter(j ⇒ j.getFromTable == t && names.contains(j.getToTable))).
- sortBy(_.getFromTable).
+ sortBy(j ⇒ Math.min(names.indexOf(j.getFromTable), names.indexOf(j.getToTable))).
zipWithIndex.
map { case (join, idx) ⇒
val fromCols = join.getFromColumns.asScala
@@ -113,14 +105,33 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
val fromTab = join.getFromTable
val toTab = join.getToTable
- println(s"fromTab=$fromTab, toTab=$toTab")
-
val onCondition = fromCols.zip(toCols).
map { case (fromCol, toCol) ⇒ s"$fromTab.$fromCol = $toTab.$toCol" }.mkString(" AND ")
- val from = if (idx == 0) fromTab else ""
+ if (idx == 0) {
+ used += fromTab
+ used += toTab
- s"$from ${sql(join.getType)} $toTab ON $onCondition"
+ s"$fromTab ${sql(join.getType)} $toTab ON $onCondition"
+ }
+ else {
+ val fromAdded = used.add(fromTab)
+ val toAdded = used.add(toTab)
+
+ if (fromAdded) {
+ val reverseType =
+ join.getType match {
+ case LEFT ⇒ RIGHT
+ case RIGHT ⇒ LEFT
+ case _ ⇒ join.getType
+ }
+ s"${sql(reverseType)} $fromTab ON $onCondition"
+ }
+ else if (toAdded)
+ s"${sql(join.getType)} $toTab ON $onCondition"
+ else
+ ""
+ }
}
if (refs.length != names.length - 1)
@@ -215,25 +226,21 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
)
private def findDateColumn(initTabs: Seq[NCSqlTable]): Option[NCSqlColumn] =
- freeDateRangeOpt match {
- case Some(_) ⇒
- schemaTabs.sortBy(t ⇒ {
- // The simple algorithm, which tries to find most suitable date type column for free date condition.
- // Higher priority for tables which were detected initially.
- val weight1 = if (initTabs.contains(t)) 0 else 1
- // Higher priority for tables don't have references from another.
- // Probably these tables can have more specific records.
- val weight2 = if (!schemaJoins.exists(_.getToTable == t.getTable)) 0 else 1
-
- (weight1, weight2)
- }).flatMap(_.getDefaultDate.asScala).toStream.headOption match {
- case Some(col) ⇒ Some(col)
- case None ⇒
- logger.warn("Free date condition ignored without throwing exceptions.")
-
- None
- }
- case None ⇒ None
+ schemaTabs.sortBy(t ⇒ {
+ // The simple algorithm, which tries to find most suitable date type column for free date condition.
+ // Higher priority for tables which were detected initially.
+ val weight1 = if (initTabs.contains(t)) 0 else 1
+ // Higher priority for tables which don't have references from another.
+ // Probably these tables can have more specific records.
+ val weight2 = if (!schemaJoins.exists(_.getToTable == t.getTable)) 0 else 1
+
+ (weight1, weight2)
+ }).flatMap(_.getDefaultDate.asScala).toStream.headOption match {
+ case Some(col) ⇒ Some(col)
+ case None ⇒
+ logger.warn("Free date condition ignored without throwing exceptions.")
+
+ None
}
private def extendConditions(
@@ -318,20 +325,24 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
condsNorm.foreach(cond ⇒ { tabsNorm += schemaTabsByNames(cond.column.getTable); colsNorm += cond.column })
sortsNorm.foreach(sort ⇒ { tabsNorm += schemaTabsByNames(sort.getColumn.getTable); colsNorm += sort.getColumn })
- val freeDateColOpt = findDateColumn(tabsNorm)
-
- freeDateColOpt.toSeq.foreach(col ⇒ { tabsNorm += schemaTabsByNames(col.getTable); colsNorm += col })
+ var freeDateColOpt =
+ if (freeDateRangeOpt.isDefined) tabsNorm.flatMap(_.getDefaultDate.asScala).toStream.headOption else None
tabsNorm = tabsNorm.distinct
- colsNorm = colsNorm.distinct
- condsNorm = condsNorm.distinct
- sortsNorm = sortsNorm.distinct
-
- println("tabsNorm="+tabsNorm.map(_.getTable).mkString("|"))
val extTabs = extendTables(tabsNorm)
- println("extTabs="+extTabs.map(_.getTable).mkString("|"))
+ if (freeDateColOpt.isEmpty && freeDateRangeOpt.isDefined) {
+ freeDateColOpt = findDateColumn(tabsNorm)
+
+ freeDateColOpt.toSeq.foreach(col ⇒ { tabsNorm += schemaTabsByNames(col.getTable); colsNorm += col })
+
+ tabsNorm = tabsNorm.distinct
+ }
+
+ colsNorm = colsNorm.distinct
+ condsNorm = condsNorm.distinct
+ sortsNorm = sortsNorm.distinct
val extCols = extendColumns(colsNorm, extTabs, freeDateColOpt)
val (extConds, extParams) = extendConditions(condsNorm, extTabs, tabsNorm, freeDateColOpt)
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt b/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
index cb807be..a946c6a 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
@@ -13,6 +13,3 @@ What are the best/least performing products/categories/employee/territory/region
What are the least performing region for the last quarter?
What are the top 5 best/worst customers in CA for the last 90 days?
-
-What are the busiest shipper/supplier in CA for the last 90 days?
-What is the average/maximum/minimum unit price/discount/quantity in CA for the last 90 days?
\ No newline at end of file
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml b/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
index f0daadc..ee317c2 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
@@ -1472,7 +1472,7 @@ elements:
groups:
- "Sort criteria"
synonyms:
- - "worst {performing|*}"
+ - "{worst|least} {performing|*}"
enabledBuiltInTokens:
- "nlpcraft:date"
- "nlpcraft:num"
diff --git a/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala b/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala
new file mode 100644
index 0000000..695a2ae
--- /dev/null
+++ b/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala
@@ -0,0 +1,200 @@
+/*
+ * 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.nlpcraft.examples.sql
+
+import java.util
+
+import org.apache.nlpcraft.examples.sql.db.SqlServer
+import org.apache.nlpcraft.model.NCElement
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.NCTestSortTokenType._
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.{NCDefaultTestModel, NCEnricherBaseSpec, NCTestDateToken => dte, NCTestLimitToken => lim, NCTestNlpToken => nlp, NCTestSortToken => srt, NCTestUserToken => usr}
+import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
+
+class NCSqlModelWrapper extends NCDefaultTestModel {
+ private val delegate = new SqlModel()
+
+ override def getElements: util.Set[NCElement] = delegate.getElements
+
+ override def getMacros: util.Map[String, String] = delegate.getMacros
+
+ override def getExamples: util.Set[String] = delegate.getExamples
+}
+
+class NCSqlModelSpec extends NCEnricherBaseSpec {
+ override def getModelClass: Option[Class[_ <: NCDefaultTestModel]] = Some(classOf[NCSqlModelWrapper])
+
+ @BeforeEach
+ override def setUp(): Unit = {
+ SqlServer.start()
+
+ super.setUp()
+ }
+
+ @AfterEach
+ override def tearDown(): Unit = {
+ super.tearDown()
+
+ SqlServer.stop()
+ }
+
+ /**
+ *
+ * @throws Exception
+ */
+ @Test
+ def test(): Unit = {
+ runBatch(
+ _ ⇒ checkExists(
+ txt = "order date",
+ usr(text = "order date", id = "col:date")
+ ),
+ _ ⇒ checkExists(
+ txt = "show me the order dates",
+ nlp(text = "show me the", isStop = true),
+ usr(text = "order dates", id = "col:date")
+ ),
+ _ ⇒ checkExists(
+ txt = "list dates of orders",
+ nlp(text = "list", isStop = true),
+ usr(text = "dates orders", id = "col:date"),
+ nlp(text = "of")
+ ),
+ _ ⇒ checkExists(
+ txt = "orders for last month",
+ usr(text = "orders", id = "tbl:orders"),
+ dte(text = "for last month")
+ ),
+ _ ⇒ checkExists(
+ txt = "shippers data",
+ usr(text = "shippers", id = "tbl:shippers"),
+ nlp(text = "data")
+ ),
+ _ ⇒ checkExists(
+ txt = "show me orders with freight more than 10 for last year",
+ nlp(text = "show me", isStop = true),
+ srt(text = "orders with", typ = BY_ONLY, note = "condition:num", index = 2),
+ usr(text = "freight more than 10", id = "condition:num"),
+ dte(text = "for last year")
+ ),
+ _ ⇒ checkExists(
+ txt = "territories data",
+ usr(text = "territories", id = "tbl:territories"),
+ nlp(text = "data")
+ ),
+ _ ⇒ checkExists(
+ txt = "employees territories",
+ usr(text = "employees territories", id = "tbl:employee_territories")
+ ),
+ _ ⇒ checkExists(
+ txt = "10 suppliers",
+ lim(text = "10", limit=10, index =1, note="tbl:suppliers"),
+ usr(text = "suppliers", id = "tbl:suppliers")
+ ),
+ // TODO: ? best ?variant
+ _ ⇒ checkExists(
+ txt = "last year Exotic Liquids orders",
+ dte(text="last year"),
+ usr(text = "Exotic Liquids", id = "condition:value"),
+ srt(text = "orders", typ = SUBJ_ONLY, note = "condition:value", index = 1)
+ ),
+ _ ⇒ checkExists(
+ txt = "give me the orders sorted by ship date",
+ nlp(text = "give me the", isStop = true),
+ usr(text = "orders", id = "tbl:orders"),
+ srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3),
+ usr(text = "ship date", id = "col:date"),
+ ),
+ _ ⇒ checkExists(
+ txt = "give me the orders sorted by ship date",
+ nlp(text = "give me the", isStop = true),
+ usr(text = "orders", id = "tbl:orders"),
+ srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3),
+ usr(text = "ship date", id = "col:date"),
+ ),
+ _ ⇒ checkExists(
+ txt = "give me the orders sorted by ship date asc",
+ nlp(text = "give me the", isStop = true),
+ usr(text = "orders", id = "tbl:orders"),
+ srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3, asc = true),
+ usr(text = "ship date", id = "col:date"),
+ nlp(text = "asc", isStop = true)
+ ),
+ _ ⇒ checkExists(
+ txt = "give me the orders sorted by date",
+ nlp(text = "give me the", isStop = true),
+ usr(text = "orders date", id = "col:date"),
+ nlp(text = "sorted"),
+ nlp(text = "by")
+ ),
+ // TODO: winner?
+ _ ⇒ checkExists(
+ txt = "What are the top orders for the last 2 weeks sorted by order quantity?",
+ lim(text = "What are the top", limit = 10, index = 1, note = "tbl:orders", asc = false),
+ usr(text = "orders", id = "tbl:orders"),
+ dte(text = "for last 2 weeks"),
+ nlp(text = "the", isStop = true),
+ srt(text = "sorted by", typ = BY_ONLY, note = "tbl:orders", index = 5),
+ usr(text = "order", id = "tbl:orders"),
+ nlp(text = "quantity"),
+ nlp(text = "?", isStop = true)
+ ),
+ // TODO: limit top
+ _ ⇒ checkExists(
+ txt = "What are the top 25 orders for the last 2 weeks sorted by order quantity?",
+ nlp(text = "What are the", isStop = true),
+ nlp(text = "top"),
+ lim(text = "25", limit = 25, index = 3, note = "tbl:orders"),
+ usr(text = "orders", id = "tbl:orders"),
+ dte(text = "for last 2 weeks"),
+ nlp(text = "the", isStop = true),
+ srt(text = "sorted by", typ = BY_ONLY, note = "col:num", index = 7),
+ usr(text = "order quantity", id = "col:num"),
+ nlp(text = "?", isStop = true)
+ ),
+ // TODO: add.
+// What are the best performing products for the last quarter?
+// What are the best performing categories for the last quarter?
+// What are the best performing employee for the last quarter?
+// What are the best performing territory for the last quarter?
+// What are the best performing region for the last quarter?
+//
+// What are the least performing products for the last quarter?
+// What are the least performing categories for the last quarter?
+// What are the least performing employee for the last quarter?
+// What are the least performing territory for the last quarter?
+// What are the least performing region for the last quarter?
+ _ ⇒ checkExists(
+ txt = "What are the best performing products for the last quarter?",
+ nlp(text = "What are the", isStop = true),
+ usr(text = "best performing", id = "sort:best"),
+ usr(text = "products", id = "tbl:products"),
+ dte(text = "for last quarter"),
+ nlp(text = "the ?", isStop = true)
+ ),
+ _ ⇒ checkExists(
+ txt = "What are the least performing categories for the last quarter?",
+ nlp(text = "What are the", isStop = true),
+ usr(text = "least performing", id = "sort:worst"),
+ usr(text = "categories", id = "tbl:categories"),
+ dte(text = "for last quarter"),
+ nlp(text = "the ?", isStop = true)
+ )
+
+ )
+ }
+}
diff --git a/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala b/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
index cd436ad..aeb2d80 100644
--- a/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
+++ b/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
@@ -32,7 +32,7 @@ class NCEnricherBaseSpec {
def getModelClass: Option[Class[_ <: NCDefaultTestModel]] = Some(classOf[NCDefaultTestModel])
@BeforeEach
- private[enrichers] def setUp(): Unit = {
+ protected def setUp(): Unit = {
val mdlId = NCDefaultTestModel.ID
getModelClass match {
@@ -50,7 +50,7 @@ class NCEnricherBaseSpec {
}
@AfterEach
- private[enrichers] def tearDown(): Unit = {
+ protected def tearDown(): Unit = {
if (client != null)
client.close()
@@ -63,7 +63,7 @@ class NCEnricherBaseSpec {
* @param txt
* @param expToks
*/
- private[enrichers] def checkExists(txt: String, expToks: NCTestToken*): Unit = {
+ protected def checkExists(txt: String, expToks: NCTestToken*): Unit = {
val res = client.ask(txt)
if (res.isFailed)
@@ -86,7 +86,7 @@ class NCEnricherBaseSpec {
* @param txt
* @param expToks
*/
- private[enrichers] def checkAll(txt: String, expToks: Seq[NCTestToken]*): Unit = {
+ protected def checkAll(txt: String, expToks: Seq[NCTestToken]*): Unit = {
val res = client.ask(txt)
if (res.isFailed)
@@ -113,7 +113,7 @@ class NCEnricherBaseSpec {
*
* @param tests
*/
- private[enrichers] def runBatch(tests: Unit ⇒ Unit*): Unit = {
+ protected def runBatch(tests: Unit ⇒ Unit*): Unit = {
val errs = tests.zipWithIndex.flatMap { case (test, i) ⇒
try {
test.apply(())