This is an automated email from the ASF dual-hosted git repository.

gortiz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new add121e6a2 Add javadoc and improve usability of FluentQueryTest 
(#13817)
add121e6a2 is described below

commit add121e6a2ec35370ead5c6dd94d32fa8d15e7ba
Author: Gonzalo Ortiz Jaureguizar <[email protected]>
AuthorDate: Mon Sep 2 15:55:19 2024 +0200

    Add javadoc and improve usability of FluentQueryTest (#13817)
    
    This adds:
    
    * FluentQueryTest.open(), which returns a FluentQueryTest.Closeable that 
can be used in try-with-resources manner.
    * FluentQueryTest.test(Consumer<FluentQueryTest>) that can be used with a 
consumer (usually a lambda).
---
 .../org/apache/pinot/queries/FluentQueryTest.java  | 241 +++++++++++++++++++--
 .../AbstractAggregationQueryBenchmark.java         |   4 +-
 2 files changed, 227 insertions(+), 18 deletions(-)

diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/FluentQueryTest.java 
b/pinot-core/src/test/java/org/apache/pinot/queries/FluentQueryTest.java
index f3982e65e5..76fcc97125 100644
--- a/pinot-core/src/test/java/org/apache/pinot/queries/FluentQueryTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/queries/FluentQueryTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.pinot.queries;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import java.io.File;
 import java.io.FileWriter;
@@ -32,6 +33,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.commons.csv.CSVFormat;
@@ -56,36 +58,125 @@ import org.intellij.lang.annotations.Language;
 import org.testng.Assert;
 
 
+/**
+ * A fluent API for testing single-stage queries.
+ *
+ * Use {@link #withBaseDir(File)} to start a new test.
+ *
+ * This test framework is intended to be used in a way to write semantically 
rich tests that are easy to read.
+ * They are more useful when small amount of data is needed to test a specific 
query.
+ *
+ * By default, this framework creates a single broker and a single server.
+ * This should be enough for most tests.
+ * But some tests may need to create more than one server.
+ * The framework is able to create up to two servers, internally called 
<em>instances</em>.
+ * The DSL force the user to create the first instance before executing a 
query, but the second instance can be created
+ * by calling {@link OnFirstInstance#andOnSecondInstance(Object[]...)}.
+ *
+ * @see 
org.apache.pinot.core.query.aggregation.function.CountAggregationFunctionTest
+ */
 public class FluentQueryTest {
 
   private final FluentBaseQueriesTest _baseQueriesTest;
-  private final File _baseDir;
+  final File _baseDir;
   private final Map<String, String> _extraQueryOptions = new HashMap<>();
 
-  private FluentQueryTest(FluentBaseQueriesTest baseQueriesTest, File baseDir) 
{
+  FluentQueryTest(FluentBaseQueriesTest baseQueriesTest, File baseDir) {
     _baseQueriesTest = baseQueriesTest;
     _baseDir = baseDir;
   }
 
+  /**
+   * Start a new test with the given base directory.
+   *
+   * Usually the base directory will be created before every test and 
destroyed after that using lifecycle testing
+   * hooks like {@link org.testng.annotations.BeforeClass} and {@link 
org.testng.annotations.AfterClass}.
+   *
+   * Each test will create its own subdirectory in the base directory, so 
multiple tests may use the same base
+   * directory.
+   *
+   * @param baseDir the base directory for the test. It must exist, be a 
directory and be writable.
+   * @return The fluent API for testing queries, where eventually {@link 
#givenTable(Schema, TableConfig)} will be
+   * called.
+   */
   public static FluentQueryTest withBaseDir(File baseDir) {
+    Preconditions.checkArgument(baseDir.exists(), "Base directory must exist");
+    Preconditions.checkArgument(baseDir.isDirectory(), "Base directory must be 
a directory");
+    Preconditions.checkArgument(baseDir.canWrite(), "Base directory must be 
writable");
     return new FluentQueryTest(new FluentBaseQueriesTest(), baseDir);
   }
 
+  /**
+   * Creates a new test with a temporary directory.
+   *
+   * @param consumer the test to run. The received FluentQueryTest will use a 
temporary directory that will be removed
+   *                 after the consumer is executed, even if a throwable is 
thrown.
+   */
+  public static void test(Consumer<FluentQueryTest> consumer) {
+    StackWalker walker = 
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
+    try (Closeable test = new 
Closeable(walker.getCallerClass().getSimpleName())) {
+      consumer.accept(test);
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  /**
+   * Creates a new test with a temporary directory.
+   *
+   * The returned object is intended to be used in a try-with-resources manner.
+   * Its close method will remove the temporary directory.
+   */
+  public static FluentQueryTest.Closeable open() {
+    StackWalker walker = 
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
+    try {
+      return new 
FluentQueryTest.Closeable(walker.getCallerClass().getSimpleName());
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  /**
+   * Sets the given extra query options to the queries that will be executed 
on this test.
+   *
+   * Older properties (including null handling) will be removed.
+   */
   public FluentQueryTest withExtraQueryOptions(Map<String, String> 
extraQueryOptions) {
     _extraQueryOptions.clear();
     _extraQueryOptions.putAll(extraQueryOptions);
     return this;
   }
 
+  /**
+   * Sets the null handling to the queries that will be executed on this test.
+   */
   public FluentQueryTest withNullHandling(boolean enabled) {
     _extraQueryOptions.put("enableNullHandling", Boolean.toString(enabled));
     return this;
   }
 
+  /**
+   * Declares a table with the given schema and table configuration.
+   *
+   * @return a {@link DeclaringTable} object to declare the segments of the 
table.
+   */
   public DeclaringTable givenTable(Schema schema, TableConfig tableConfig) {
     return new DeclaringTable(_baseQueriesTest, tableConfig, schema, _baseDir, 
_extraQueryOptions);
   }
 
+  public static class Closeable extends FluentQueryTest implements 
AutoCloseable {
+
+    private Closeable(String testBaseName)
+        throws IOException {
+      super(new FluentBaseQueriesTest(), 
Files.createTempDirectory(testBaseName).toFile());
+    }
+
+    @Override
+    public void close() {
+      _baseDir.delete();
+    }
+  }
+
   public static class DeclaringTable {
     private final FluentBaseQueriesTest _baseQueriesTest;
     private final TableConfig _tableConfig;
@@ -100,17 +191,33 @@ public class FluentQueryTest {
       _schema = schema;
       _baseDir = baseDir;
       _extraQueryOptions = extraQueryOptions;
+      Preconditions.checkArgument(_schema.getSchemaName() != null, "Schema 
must have a name");
     }
 
-    public OnFirstInstance getFirstInstance() {
+    /**
+     * Moves the fluent DSL to the first instance (aka server).
+     * @return
+     */
+    public OnFirstInstance onFirstInstance() {
       return new OnFirstInstance(_tableConfig, _schema, _baseDir, false, 
_baseQueriesTest, _extraQueryOptions);
     }
 
+    /**
+     * Creates one segment on the first instance (aka server) with the given 
content.
+     *
+     * @param content the content of the segment.
+     * @see OnFirstInstance#andSegment(String...) to learn more about the 
content syntax
+     */
     public OnFirstInstance onFirstInstance(String... content) {
       return new OnFirstInstance(_tableConfig, _schema, _baseDir, false, 
_baseQueriesTest, _extraQueryOptions)
           .andSegment(content);
     }
 
+    /**
+     * Creates one segment on the first instance (aka server) with the given 
content.
+     * @param content the content of the segment. Each element of the array is 
a row. Each row is an array of objects
+     *                that should be compatible with the table definition.
+     */
     public OnFirstInstance onFirstInstance(Object[]... content) {
       return new OnFirstInstance(_tableConfig, _schema, _baseDir, false, 
_baseQueriesTest, _extraQueryOptions)
           .andSegment(content);
@@ -197,15 +304,33 @@ public class FluentQueryTest {
       _segmentContents.clear();
     }
 
+    /**
+     * Executes the given query and returns an object that can be used to 
assert the results.
+     */
     public QueryExecuted whenQuery(@Language("sql") String query) {
       processSegments();
       BrokerResponseNative brokerResponse = 
_baseQueriesTest.getBrokerResponse(query, _extraQueryOptions);
       return new QueryExecuted(_baseQueriesTest, brokerResponse, 
_extraQueryOptions);
     }
 
+    /**
+     * Creates another table.
+     *
+     * The older tables can still be used.
+     */
     public DeclaringTable givenTable(Schema schema, TableConfig tableConfig) {
+      processSegments();
       return new DeclaringTable(_baseQueriesTest, tableConfig, schema, 
_indexDir.getParentFile(), _extraQueryOptions);
     }
+
+    public TableWithSegments prepareToQuery() {
+      processSegments();
+      return this;
+    }
+
+    public void tearDown() {
+      _baseQueriesTest.shutdownExecutor();
+    }
   }
 
   public static class OnFirstInstance extends TableWithSegments {
@@ -214,23 +339,59 @@ public class FluentQueryTest {
       super(tableConfig, schema, baseDir, onSecondInstance, baseQueriesTest, 
extraQueryOptions);
     }
 
+    /**
+     * Adds a new segment to the table in this instance.
+     * @param content the content of the segment. Each element of the array is 
a row. Each row is an array of objects
+     *                that should be compatible with the table definition.
+     */
     public OnFirstInstance andSegment(Object[]... content) {
       _segmentContents.add(new FakeSegmentContent(content));
       return this;
     }
 
+    /**
+     * Adds a new segment to the table in this instance.
+     *
+     * The content is a table in text format. The first row is the header, and 
the rest of the rows are the data.
+     * Each column must be separated by pipes ({@code |}).
+     * The header must be the name of the column (as declared in the schema).
+     * The order of the columns doesn't have to match the order of the columns 
in the schema.
+     *
+     * After the header, each row must have the same number of columns as the 
header and will contain the data.
+     * Each entry in the row must be a valid value for the column type.
+     * The rules to parse these values are:
+     * <ol>
+     *   <li>First, the value will be trimmed</li>
+     *   <li>{@code null} will always be treated as null</li>
+     *   <li>{@code "null"} will be parsed as
+     *   {@link PinotDataType#convert(Object, PinotDataType) 
PinotDataType.convert("null", type)}</li>
+     *   <li>Any other value will be parsed as
+     *   {@link PinotDataType#convert(Object, PinotDataType) 
PinotDataType.convert(value, type)}</li>
+     * </ol>
+     *
+     * @param tableText the content of the segment, as explained above.
+     */
     public OnFirstInstance andSegment(String... tableText) {
       super.andSegment(tableText);
       return this;
     }
 
-    public OnSecondInstance getSecondInstance() {
+    /**
+     * Moves the fluent DSL to the second instance (aka server).
+     */
+    public OnSecondInstance andOnSecondInstance() {
       processSegments();
       return new OnSecondInstance(
           _tableConfig, _schema, _indexDir.getParentFile(), 
!_onSecondInstance, _baseQueriesTest, _extraQueryOptions
       );
     }
 
+    /**
+     * Moves the fluent DSL to the second instance (aka server), adding the 
content as the first segment.
+     *
+     * @param content the content of the segment. Each element of the array is 
a row. Each row is an array of objects
+     *                that should be compatible with the table definition.
+     */
     public OnSecondInstance andOnSecondInstance(Object[]... content) {
       processSegments();
       return new OnSecondInstance(
@@ -238,6 +399,12 @@ public class FluentQueryTest {
           .andSegment(content);
     }
 
+    /**
+     * Creates one segment on a second instance (aka server).
+     *
+     * @param content the content of the segment.
+     * @see OnFirstInstance#andSegment(String...) to learn more about the 
content syntax
+     */
     public OnSecondInstance andOnSecondInstance(String... content) {
       processSegments();
       return new OnSecondInstance(
@@ -246,13 +413,9 @@ public class FluentQueryTest {
     }
 
     public OnFirstInstance prepareToQuery() {
-      processSegments();
+      super.prepareToQuery();
       return this;
     }
-
-    public void tearDown() {
-      _baseQueriesTest.shutdownExecutor();
-    }
   }
 
   public static class OnSecondInstance extends TableWithSegments {
@@ -261,24 +424,30 @@ public class FluentQueryTest {
       super(tableConfig, schema, baseDir, onSecondInstance, baseQueriesTest, 
extraQueryOptions);
     }
 
+    /**
+     * Adds a new segment to the table in this instance.
+     * @param content the content of the segment. Each element of the array is 
a row. Each row is an array of objects
+     *                that should be compatible with the table definition.
+     */
     public OnSecondInstance andSegment(Object[]... content) {
       _segmentContents.add(new FakeSegmentContent(content));
       return this;
     }
 
+    /**
+     * Adds a new segment to the table in this instance.
+     * @param tableText the content of the segment.
+     * @see OnFirstInstance#andSegment(String...) to learn more about the
+     */
     public OnSecondInstance andSegment(String... tableText) {
       super.andSegment(tableText);
       return this;
     }
 
     public OnSecondInstance prepareToQuery() {
-      processSegments();
+      super.prepareToQuery();
       return this;
     }
-
-    public void tearDown() {
-      _baseQueriesTest.shutdownExecutor();
-    }
   }
 
   public static class QueryExecuted {
@@ -293,6 +462,26 @@ public class FluentQueryTest {
       _extraQueryOptions = extraQueryOptions;
     }
 
+    /**
+     * Asserts that the result of the query is the given table.
+     *
+     * The table is a text table. The first row is the header, and the rest of 
the rows are the data.
+     * Each column must be separated by pipes ({@code |}).
+     * The header must be a valid column type (as defined by {@link 
PinotDataType}, although it will be trimmed and
+     * uppercased).
+     *
+     * After the header, each row must have the same number of columns as the 
header and will contain the data.
+     * Each entry in the row must be a valid value for the column type.
+     * The rules to parse these values are:
+     * <ol>
+     *   <li>First, the value will be trimmed</li>
+     *   <li>{@code null} will always be treated as null</li>
+     *   <li>{@code "null"} will be parsed as
+     *   {@link PinotDataType#convert(Object, PinotDataType) 
PinotDataType.convert("null", type)}</li>
+     *   <li>Any other value will be parsed as
+     *   {@link PinotDataType#convert(Object, PinotDataType) 
PinotDataType.convert(value, type)}</li>
+     * </ol>
+     */
     public QueryExecuted thenResultIs(String... tableText) {
       Object[][] rows = tableAsRows(
           headerCells -> Arrays.stream(headerCells)
@@ -307,6 +496,9 @@ public class FluentQueryTest {
       return this;
     }
 
+    /**
+     * Asserts that the result of the query is the given table.
+     */
     public QueryExecuted thenResultIs(Object[]... expectedResult) {
       if (_brokerResponse.getExceptionsSize() > 0) {
         Assert.fail("Query failed with " + _brokerResponse.getExceptions());
@@ -317,7 +509,8 @@ public class FluentQueryTest {
       for (int i = 0; i < rowsToAnalyze; i++) {
         Object[] actualRow = actualRows.get(i);
         Object[] expectedRow = expectedResult[i];
-        for (int j = 0; j < actualRow.length; j++) {
+        int colsToAnalyze = Math.min(actualRow.length, expectedRow.length);
+        for (int j = 0; j < colsToAnalyze; j++) {
           Object actualCell = actualRow[j];
           Object expectedCell = expectedRow[j];
           if (actualCell != null && expectedCell != null) {
@@ -333,24 +526,40 @@ public class FluentQueryTest {
             Assert.assertEquals(actualCell, expectedCell, "On row " + i + " 
and column " + j);
           }
         }
+        Assert.assertEquals(actualRow.length, expectedRow.length, "Unexpected 
number of columns on row " + i);
       }
       Assert.assertEquals(actualRows.size(), expectedResult.length, 
"Unexpected number of rows");
       return this;
     }
 
+    /**
+     * Sets the given extra query options to the queries that will be executed 
on this test.
+     *
+     * Older properties (including null handling) will be removed.
+     */
     public QueryExecuted withExtraQueryOptions(Map<String, String> 
extraQueryOptions) {
       _extraQueryOptions.clear();
       _extraQueryOptions.putAll(extraQueryOptions);
       return this;
     }
 
+    /**
+     * Sets the null handling to the queries that will be executed on this 
test.
+     *
+     * <strong>Important:</strong> This change will only affect new queries.
+     */
     public QueryExecuted withNullHandling(boolean enabled) {
       _extraQueryOptions.put("enableNullHandling", Boolean.toString(enabled));
       return this;
     }
 
+    /**
+     * Executes the given query and returns an object that can be used to 
assert the results.
+     *
+     * The tables and segments already created can still be used.
+     */
     public QueryExecuted whenQuery(@Language("sql") String query) {
-      BrokerResponseNative brokerResponse = 
_baseQueriesTest.getBrokerResponse(query);
+      BrokerResponseNative brokerResponse = 
_baseQueriesTest.getBrokerResponse(query, _extraQueryOptions);
       return new QueryExecuted(_baseQueriesTest, brokerResponse, 
_extraQueryOptions);
     }
   }
diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/AbstractAggregationQueryBenchmark.java
 
b/pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/AbstractAggregationQueryBenchmark.java
index 7e8a3ded16..7a3c21c51f 100644
--- 
a/pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/AbstractAggregationQueryBenchmark.java
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/aggregation/AbstractAggregationQueryBenchmark.java
@@ -48,14 +48,14 @@ public abstract class AbstractAggregationQueryBenchmark {
         FluentQueryTest.withBaseDir(_baseDir)
             .withNullHandling(nullHandlingEnabled)
             .givenTable(schema, tableConfig)
-            .getFirstInstance();
+            .onFirstInstance();
 
     List<Object[][]> segmentsOnFirstServer = segmentsPerServer.get(0);
     for (Object[][] segment : segmentsOnFirstServer) {
       onFirstInstance.andSegment(segment);
     }
 
-    FluentQueryTest.OnSecondInstance onSecondInstance = 
onFirstInstance.getSecondInstance();
+    FluentQueryTest.OnSecondInstance onSecondInstance = 
onFirstInstance.andOnSecondInstance();
     List<Object[][]> segmentsOnSecondServer = segmentsPerServer.get(1);
     for (Object[][] segment : segmentsOnSecondServer) {
       onSecondInstance.andSegment(segment);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to