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

Add new interfaces to extend DefaultLocale and DefaultTimeZone #770

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/DefaultLocale.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,11 @@
*/
String variant() default "";

/**
* A class implementing {@link LocaleProvider} to be used for custom {@code Locale} resolution.
* This is mutually exclusive with other properties, if any other property is given a value it
* will result in an {@link org.junit.jupiter.api.extension.ExtensionConfigurationException}.
*/
Class<? extends LocaleProvider> localeProvider() default LocaleProvider.NullLocaleProvider.class;

}
36 changes: 32 additions & 4 deletions src/main/java/org/junitpioneer/jupiter/DefaultLocaleExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junitpioneer.internal.PioneerAnnotationUtils;
import org.junitpioneer.internal.PioneerUtils;
import org.junitpioneer.jupiter.LocaleProvider.NullLocaleProvider;

class DefaultLocaleExtension implements BeforeEachCallback, AfterEachCallback {

Expand Down Expand Up @@ -49,20 +51,26 @@ private void storeDefaultLocale(ExtensionContext context) {
private static Locale createLocale(DefaultLocale annotation) {
if (!annotation.value().isEmpty()) {
return createFromLanguageTag(annotation);
} else {
} else if (!annotation.language().isEmpty()) {
return createFromParts(annotation);
} else {
return getFromProvider(annotation);
}
}

private static Locale createFromLanguageTag(DefaultLocale annotation) {
if (!annotation.language().isEmpty() || !annotation.country().isEmpty() || !annotation.variant().isEmpty()) {
if (!annotation.language().isEmpty() || !annotation.country().isEmpty() || !annotation.variant().isEmpty()
|| annotation.localeProvider() != NullLocaleProvider.class) {
throw new ExtensionConfigurationException(
"@DefaultLocale can only be used with language tag if language, country, and variant are not set");
"@DefaultLocale can only be used with language tag if language, country, variant and provider are not set");
}
return Locale.forLanguageTag(annotation.value());
}

private static Locale createFromParts(DefaultLocale annotation) {
if (annotation.localeProvider() != NullLocaleProvider.class)
throw new ExtensionConfigurationException(
"@DefaultLocale can only be used with language tag if provider is not set");
String language = annotation.language();
String country = annotation.country();
String variant = annotation.variant();
Expand All @@ -75,8 +83,28 @@ private static Locale createFromParts(DefaultLocale annotation) {
} else {
throw new ExtensionConfigurationException(
"@DefaultLocale not configured correctly. When not using a language tag, specify either"
+ "language, or language and country, or language and country and variant.");
+ " language, or language and country, or language and country and variant.");
}
}

private static Locale getFromProvider(DefaultLocale annotation) {
if (!annotation.value().isEmpty() || !annotation.language().isEmpty() || !annotation.country().isEmpty()
|| !annotation.variant().isEmpty())
throw new ExtensionConfigurationException(
"@DefaultLocale can only be used with a provider if value, language, country and variant are not set.");
var providerClass = annotation.localeProvider();
LocaleProvider provider;
try {
provider = ReflectionSupport.newInstance(providerClass);
}
catch (Exception exception) {
throw new ExtensionConfigurationException(
"LocaleProvider instance could not be constructed because of an exception", exception);
}
var locale = provider.get();
if (locale == null)
throw new NullPointerException("LocaleProvider instance returned with null");
return locale;
}

@Override
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/org/junitpioneer/jupiter/DefaultTimeZone.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@
* "GMT-8:00". Note that the support of abbreviations is for JDK 1.1.x
* compatibility only and full names should be used.
*/
String value();
String value() default "";

/**
* A class implementing {@link TimeZoneProvider} to be used for custom {@code TimeZone} resolution.
* This is mutually exclusive with other properties, if any other property is given a value it
* will result in an {@link org.junit.jupiter.api.extension.ExtensionConfigurationException}.
*/
Class<? extends TimeZoneProvider> timeZoneProvider() default TimeZoneProvider.NullTimeZoneProvider.class;

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junitpioneer.jupiter;

import java.util.Optional;
import java.util.TimeZone;

import org.junit.jupiter.api.extension.AfterEachCallback;
Expand All @@ -18,7 +19,9 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junitpioneer.internal.PioneerAnnotationUtils;
import org.junitpioneer.jupiter.TimeZoneProvider.NullTimeZoneProvider;

class DefaultTimeZoneExtension implements BeforeEachCallback, AfterEachCallback {

Expand All @@ -34,14 +37,28 @@ public void beforeEach(ExtensionContext context) {
}

private void setDefaultTimeZone(Store store, DefaultTimeZone annotation) {
TimeZone defaultTimeZone = createTimeZone(annotation.value());
validateCorrectConfiguration(annotation);
TimeZone defaultTimeZone;
if (annotation.timeZoneProvider() != NullTimeZoneProvider.class)
defaultTimeZone = createTimeZone(annotation.timeZoneProvider());
else
defaultTimeZone = createTimeZone(annotation.value());
// defer storing the current default time zone until the new time zone could be created from the configuration
// (this prevents cases where misconfigured extensions store default time zone now and restore it later,
// which leads to race conditions in our tests)
storeDefaultTimeZone(store);
TimeZone.setDefault(defaultTimeZone);
}

private static void validateCorrectConfiguration(DefaultTimeZone annotation) {
boolean noValue = annotation.value().isEmpty();
boolean noProvider = annotation.timeZoneProvider() == NullTimeZoneProvider.class;
if (noValue == noProvider)
throw new ExtensionConfigurationException(
"Either a valid time zone id or a TimeZoneProvider must be provided to "
+ DefaultTimeZone.class.getSimpleName());
}

private static TimeZone createTimeZone(String timeZoneId) {
TimeZone configuredTimeZone = TimeZone.getTimeZone(timeZoneId);
// TimeZone::getTimeZone returns with GMT as fallback if the given ID cannot be understood
Expand All @@ -55,6 +72,17 @@ private static TimeZone createTimeZone(String timeZoneId) {
return configuredTimeZone;
}

private static TimeZone createTimeZone(Class<? extends TimeZoneProvider> providerClass) {
try {
TimeZoneProvider provider = ReflectionSupport.newInstance(providerClass);
return Optional.ofNullable(provider.get()).orElse(TimeZone.getTimeZone("GMT"));
}
catch (Exception exception) {
throw new ExtensionConfigurationException("Could not instantiate TimeZoneProvider because of exception",
exception);
}
}

private void storeDefaultTimeZone(Store store) {
store.put(KEY, TimeZone.getDefault());
}
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/LocaleProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2016-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter;

import java.util.Locale;
import java.util.function.Supplier;

public interface LocaleProvider extends Supplier<Locale> {

interface NullLocaleProvider extends LocaleProvider {
}

}
21 changes: 21 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/TimeZoneProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2016-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter;

import java.util.TimeZone;
import java.util.function.Supplier;

public interface TimeZoneProvider extends Supplier<TimeZone> {

interface NullTimeZoneProvider extends TimeZoneProvider {
}

}
120 changes: 118 additions & 2 deletions src/test/java/org/junitpioneer/jupiter/DefaultLocaleTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,15 @@ void setForTestMethod() {
}

@AfterAll
@ReadsDefaultTimeZone
@ReadsDefaultLocale
void resetAfterTestMethodExecution() {
assertThat(Locale.getDefault().getLanguage()).isEqualTo("custom");
}

}

@AfterAll
@ReadsDefaultTimeZone
@ReadsDefaultLocale
void resetAfterTestMethodExecution() {
assertThat(Locale.getDefault().getLanguage()).isEqualTo("custom");
}
Expand Down Expand Up @@ -364,4 +364,120 @@ static class InheritanceBaseTest {

}

@Nested
@DisplayName("when used with a locale provider")
class LocaleProviderTests {

@Test
@DisplayName("can get a basic locale from provider")
@DefaultLocale(localeProvider = BasicLocaleProvider.class)
void canUseProvider() {
assertThat(Locale.getDefault()).isEqualTo(Locale.FRENCH);
}

@Test
@ReadsDefaultLocale
@DisplayName("throws a NullPointerException with custom message if provider returns null")
void providerReturnsNull() {
ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "returnsNull");

assertThat(results)
.hasSingleFailedTest()
.withExceptionInstanceOf(NullPointerException.class)
.hasMessageContaining("LocaleProvider instance returned with null");
}

@Test
@ReadsDefaultLocale
@DisplayName("throws an ExtensionConfigurationException if any other option is present")
void mutuallyExclusiveWithValue() {
ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithValue");

assertThat(results)
.hasSingleFailedTest()
.withExceptionInstanceOf(ExtensionConfigurationException.class)
.hasMessageContaining(
"can only be used with language tag if language, country, variant and provider are not set");
}

@Test
@ReadsDefaultLocale
@DisplayName("throws an ExtensionConfigurationException if any other option is present")
void mutuallyExclusiveWithLanguage() {
ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "mutuallyExclusiveWithLanguage");

assertThat(results)
.hasSingleFailedTest()
.withExceptionInstanceOf(ExtensionConfigurationException.class)
.hasMessageContaining("can only be used with language tag if provider is not set");
}

@Test
@ReadsDefaultLocale
@DisplayName("throws an ExtensionConfigurationException if localeProvider can't be constructed")
void badConstructor() {
ExecutionResults results = executeTestMethod(BadProviderTestCases.class, "badConstructor");

assertThat(results)
.hasSingleFailedTest()
.withExceptionInstanceOf(ExtensionConfigurationException.class)
.hasMessageContaining("could not be constructed because of an exception");
}

}

static class BadProviderTestCases {

@Test
@DefaultLocale(value = "en", localeProvider = BasicLocaleProvider.class)
void mutuallyExclusiveWithValue() {
}

@Test
@DefaultLocale(language = "en", localeProvider = BasicLocaleProvider.class)
void mutuallyExclusiveWithLanguage() {
}

@Test
@DefaultLocale(localeProvider = ReturnsNullLocaleProvider.class)
void returnsNull() {
}

@Test
@DefaultLocale(localeProvider = BadConstructorLocaleProvider.class)
void badConstructor() {
}

}

static class BasicLocaleProvider implements LocaleProvider {

@Override
public Locale get() {
return Locale.FRENCH;
}

}

static class ReturnsNullLocaleProvider implements LocaleProvider {

@Override
public Locale get() {
return null;
}

}

static class BadConstructorLocaleProvider implements LocaleProvider {

BadConstructorLocaleProvider(String unused) {
}

@Override
public Locale get() {
return Locale.GERMAN;
}

}

}