Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parameterized tests at class-level with JUnit5 #9161

Merged
merged 26 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c71981c
Migrate TestDictionaryRowGroupFilter
GianlucaPrincipini Nov 26, 2023
f0d3c2b
Add some comments in TestDictionaryRowGroupFilter
GianlucaPrincipini Nov 26, 2023
b240d39
Add change comment in TestDictionaryRowGroupFilter abstract class
GianlucaPrincipini Nov 26, 2023
9cae9fc
Restore final modifier in writerVersion
GianlucaPrincipini Nov 26, 2023
4d381f8
Add parameterized test extension for parquet parameterized tests
GianlucaPrincipini Nov 30, 2023
653fec1
Use Preconditions.checkState instead of assert
GianlucaPrincipini Dec 4, 2023
6bfb542
Tempdir is now private
GianlucaPrincipini Dec 4, 2023
f4f040a
Move parameterized test extension and utils in iceberg-api
GianlucaPrincipini Dec 4, 2023
833e9bf
Move TestHelpers from iceberg-parquet to iceberg-api, use assertj ass…
GianlucaPrincipini Dec 4, 2023
2fb4dc9
Run spotlessApply
GianlucaPrincipini Dec 4, 2023
54c0d52
Change ambiguous "value" parameter value to "index"
GianlucaPrincipini Dec 9, 2023
d14498b
Migrate TestIcebergInputFormats to Junit5 Parameterized test, adapt p…
GianlucaPrincipini Dec 9, 2023
b09652c
Use assertj style assertions in TestIcebergInputFormats
GianlucaPrincipini Dec 10, 2023
27cd4ce
Clean imports
GianlucaPrincipini Dec 10, 2023
a8e9f19
Update license
GianlucaPrincipini Dec 10, 2023
d312a04
Fix formatting
GianlucaPrincipini Dec 10, 2023
ea33322
Remove assertThrows in favor of Assertions.assertThatThrownBy
GianlucaPrincipini Dec 11, 2023
5217c6c
Add newlines
GianlucaPrincipini Dec 11, 2023
d4dc6c7
Change license and set references to Flink code
GianlucaPrincipini Dec 11, 2023
4ecdf80
Fix formatting
GianlucaPrincipini Dec 11, 2023
5d051c5
Update api/src/test/java/org/apache/iceberg/ParameterizedTestExtensio…
GianlucaPrincipini Dec 12, 2023
7dd8885
Revert non-related commits
GianlucaPrincipini Dec 12, 2023
39d0837
Fix Preconditions import
GianlucaPrincipini Dec 12, 2023
eaf4c6d
Fix checkstyle violations
GianlucaPrincipini Dec 12, 2023
245e411
Revert changes other than TestDictionaryRowGroupFilter
GianlucaPrincipini Dec 13, 2023
d2cd285
static import
nastra Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,15 @@ This product includes code from Apache HttpComponents Client.
Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://hc.apache.org/
License: https://www.apache.org/licenses/LICENSE-2.0

--------------------------------------------------------------------------------

This product includes code from Apache Flink.

* Parameterized test at class level logic in ParameterizedTestExtension.java
* Parameter provider annotation for parameterized tests in Parameters.java
* Parameter field annotation for parameterized tests in Parameter.java

Copyright: 1999-2022 The Apache Software Foundation.
Home page: https://flink.apache.org/
License: https://www.apache.org/licenses/LICENSE-2.0
48 changes: 48 additions & 0 deletions api/src/test/java/org/apache/iceberg/Parameter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.iceberg;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.runners.Parameterized;

/**
* The annotation is used to replace {@link Parameterized.Parameter} for Junit 5 parameterized
* tests.
nastra marked this conversation as resolved.
Show resolved Hide resolved
*
* <p>This implementation has been taken from Flink repository. The only difference is the "index"
* field, renamed to be more intuitive
*
* @see <a
* href="https://github.com/apache/flink/blob/master/flink-test-utils-parent/flink-test-utils-junit/src/main/java/org/apache/flink/testutils/junit/extensions/parameterized/Parameter.java">
* Parameters</a>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Parameter {
/**
* Position for the parameter in each parameters Collection item. Assuming that parameterized test
* has only one Parameter this index is set to 0 by default
*
* @return
*/
int index() default 0;
}
254 changes: 254 additions & 0 deletions api/src/test/java/org/apache/iceberg/ParameterizedTestExtension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
* 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.iceberg;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.assertj.core.util.Preconditions;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually the wrong import. Should be org.apache.iceberg.relocated.com.google.common.base.Preconditions

import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.support.HierarchyTraversalMode;

/**
* This extension is used to implement parameterized tests for Junit 5 to replace Parameterized in
* Junit4.
*
* <p>When use this extension, all tests must be annotated by {@link TestTemplate}.
GianlucaPrincipini marked this conversation as resolved.
Show resolved Hide resolved
*
* <p>This implementation has been taken from Flink repository, to provide test parameterization at
* class-level in Junit5. The only difference consists in using Assertj preconditions.
GianlucaPrincipini marked this conversation as resolved.
Show resolved Hide resolved
*
* @see <a
* href="https://github.com/apache/flink/blob/master/flink-test-utils-parent/flink-test-utils-junit/src/main/java/org/apache/flink/testutils/junit/extensions/parameterized/ParameterizedTestExtension.java">
* ParameterizedTestExtension</a>
*/
public class ParameterizedTestExtension implements TestTemplateInvocationContextProvider {

private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create("parameterized");
private static final String PARAMETERS_STORE_KEY = "parameters";
private static final String PARAMETER_FIELD_STORE_KEY_PREFIX = "parameterField_";
private static final String INDEX_TEMPLATE = "{index}";

@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {

// Search method annotated with @Parameters
final List<Method> parameterProviders =
AnnotationSupport.findAnnotatedMethods(
context.getRequiredTestClass(), Parameters.class, HierarchyTraversalMode.TOP_DOWN);
if (parameterProviders.isEmpty()) {
throw new IllegalStateException("Cannot find any parameter provider");
}
if (parameterProviders.size() > 1) {
throw new IllegalStateException("Multiple parameter providers are found");
}

Method parameterProvider = parameterProviders.get(0);
// Get potential test name
String testNameTemplate = parameterProvider.getAnnotation(Parameters.class).name();

// Get parameter values
final Object parameterValues;
try {
parameterProvider.setAccessible(true);
parameterValues = parameterProvider.invoke(null);
context.getStore(NAMESPACE).put(PARAMETERS_STORE_KEY, parameterValues);
} catch (Exception e) {
throw new IllegalStateException("Failed to invoke parameter provider", e);
}

Preconditions.checkState(parameterValues != null, "Parameter values cannot be null");
GianlucaPrincipini marked this conversation as resolved.
Show resolved Hide resolved

// Parameter values could be Object[][]
if (parameterValues instanceof Object[][]) {
Object[][] typedParameterValues = (Object[][]) parameterValues;
return createContextForParameters(
Arrays.stream(typedParameterValues), testNameTemplate, context);
}

// or a Collection
if (parameterValues instanceof Collection) {
final Collection<?> typedParameterValues = (Collection<?>) parameterValues;
final Stream<Object[]> parameterValueStream =
typedParameterValues.stream()
.map(
(Function<Object, Object[]>)
parameterValue -> {
if (parameterValue instanceof Object[]) {
return (Object[]) parameterValue;
} else {
return new Object[] {parameterValue};
}
});
return createContextForParameters(parameterValueStream, testNameTemplate, context);
}

throw new IllegalStateException(
String.format(
"Return type of @Parameters annotated method \"%s\" should be either Object[][] or Collection",
parameterProvider));
}

private static class FieldInjectingInvocationContext implements TestTemplateInvocationContext {

private final String testNameTemplate;
private final Object[] parameterValues;

public FieldInjectingInvocationContext(String testNameTemplate, Object[] parameterValues) {
this.testNameTemplate = testNameTemplate;
this.parameterValues = parameterValues;
}

@Override
public String getDisplayName(int invocationIndex) {
if (INDEX_TEMPLATE.equals(testNameTemplate)) {
return TestTemplateInvocationContext.super.getDisplayName(invocationIndex);
} else {
return MessageFormat.format(testNameTemplate, parameterValues);
}
}

@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new FieldInjectingHook(parameterValues));
}

private static class FieldInjectingHook implements BeforeEachCallback {

private final Object[] parameterValues;

public FieldInjectingHook(Object[] parameterValues) {
this.parameterValues = parameterValues;
}

@Override
public void beforeEach(ExtensionContext context) throws Exception {
for (int i = 0; i < parameterValues.length; i++) {
getParameterField(i, context).setAccessible(true);
getParameterField(i, context).set(context.getRequiredTestInstance(), parameterValues[i]);
}
}
}
}

private static class ConstructorParameterResolverInvocationContext
implements TestTemplateInvocationContext {

private final String testNameTemplate;
private final Object[] parameterValues;

public ConstructorParameterResolverInvocationContext(
String testNameTemplate, Object[] parameterValues) {
this.testNameTemplate = testNameTemplate;
this.parameterValues = parameterValues;
}

@Override
public String getDisplayName(int invocationIndex) {
if (INDEX_TEMPLATE.equals(testNameTemplate)) {
return TestTemplateInvocationContext.super.getDisplayName(invocationIndex);
} else {
return MessageFormat.format(testNameTemplate, parameterValues);
}
}

@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ConstructorParameterResolver(parameterValues));
}

private static class ConstructorParameterResolver implements ParameterResolver {

private final Object[] parameterValues;

public ConstructorParameterResolver(Object[] parameterValues) {
this.parameterValues = parameterValues;
}

@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return true;
}

@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterValues[parameterContext.getIndex()];
}
}
}

// -------------------------------- Helper functions -------------------------------------------

private Stream<TestTemplateInvocationContext> createContextForParameters(
Stream<Object[]> parameterValueStream, String testNameTemplate, ExtensionContext context) {
// Search fields annotated by @Parameter
final List<Field> parameterFields =
AnnotationSupport.findAnnotatedFields(context.getRequiredTestClass(), Parameter.class);

// Use constructor parameter style
if (parameterFields.isEmpty()) {
return parameterValueStream.map(
parameterValue ->
new ConstructorParameterResolverInvocationContext(testNameTemplate, parameterValue));
}

// Use field injection style
for (Field parameterField : parameterFields) {
final int index = parameterField.getAnnotation(Parameter.class).index();
context.getStore(NAMESPACE).put(getParameterFieldStoreKey(index), parameterField);
}

return parameterValueStream.map(
GianlucaPrincipini marked this conversation as resolved.
Show resolved Hide resolved
parameterValue -> new FieldInjectingInvocationContext(testNameTemplate, parameterValue));
}

private static String getParameterFieldStoreKey(int parameterIndex) {
return PARAMETER_FIELD_STORE_KEY_PREFIX + parameterIndex;
}

private static Field getParameterField(int parameterIndex, ExtensionContext context) {
return (Field) context.getStore(NAMESPACE).get(getParameterFieldStoreKey(parameterIndex));
}
}
40 changes: 40 additions & 0 deletions api/src/test/java/org/apache/iceberg/Parameters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.iceberg;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* The annotation is used to replace Parameterized.Parameters(Junit4) for Junit 5 parameterized
* tests.
*
* <p>This implementation has been taken from Flink repository
*
* @see <a
* href="https://github.com/apache/flink/blob/master/flink-test-utils-parent/flink-test-utils-junit/src/main/java/org/apache/flink/testutils/junit/extensions/parameterized/Parameters.java">
* Parameters</a>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Parameters {
String name() default "{index}";
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ project(':iceberg-parquet') {
exclude group: 'org.apache.avro', module: 'avro'
}

testImplementation project(path: ':iceberg-api', configuration: 'testArtifacts')
testImplementation project(path: ':iceberg-core', configuration: 'testArtifacts')
}
}
Expand Down
Loading
Loading