diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/GarbageDisposal.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/GarbageDisposal.java index 4a25b81a1..f75394726 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/GarbageDisposal.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/GarbageDisposal.java @@ -18,6 +18,7 @@ import java.lang.ref.Cleaner; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; import org.slf4j.Logger; @@ -32,6 +33,12 @@ @API(since = "0.0.1", status = Status.INTERNAL) public final class GarbageDisposal { + // Tuning for JVMs on Windows. + private static final int THREAD_PRIORITY = Thread.MAX_PRIORITY - 1; + private static final int THREAD_STACK_SIZE = 20 * 1_024; + + private static final AtomicInteger CLEANER_THREAD_ID = new AtomicInteger(0); + private static final ThreadGroup THREAD_GROUP = new ThreadGroup("JCT GarbageDisposal thread"); private static final Logger LOGGER = LoggerFactory.getLogger(GarbageDisposal.class); private static final Lazy CLEANER = new Lazy<>(GarbageDisposal::newCleaner); @@ -63,12 +70,23 @@ public static void onPhantom(Object ref, String name, AutoCloseable hook) { private static Cleaner newCleaner() { // This thread factory has exactly 1 thread created from it. - return Cleaner.create(runnable -> { - var thread = new Thread(runnable); - thread.setName("JCT GC hook caller"); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - }); + return Cleaner.create(runnable -> newThread("cleaner thread", runnable)); + } + + private static Thread newThread(String name, Runnable runnable) { + var thread = new Thread( + THREAD_GROUP, + runnable, + "JCT GarbageDisposal - thread #" + + CLEANER_THREAD_ID.incrementAndGet() + + " - " + + name, + THREAD_STACK_SIZE, + false + ); + thread.setDaemon(false); + thread.setPriority(THREAD_PRIORITY); + return thread; } private GarbageDisposal() { @@ -87,26 +105,24 @@ private CloseableDelegate(String name, AutoCloseable closeable) { @Override public void run() { - var thread = new Thread(() -> { - try { - LOGGER.debug("Closing {} ({})", name, closeable); - closeable.close(); - } catch (Exception ex) { - var thisThread = Thread.currentThread(); - LOGGER.error( - "Failed to close {} ({}) on thread {} [{}]", - name, - closeable, - thisThread.getId(), - thisThread.getName(), - ex - ); - } - }); + newThread("dispose of " + name, this::runSync).start(); + } - thread.setName(toString()); - thread.setPriority(Thread.MAX_PRIORITY); - thread.start(); + private void runSync() { + try { + LOGGER.debug("Closing {} ({})", name, closeable); + closeable.close(); + } catch (Exception ex) { + var thisThread = Thread.currentThread(); + LOGGER.error( + "Failed to close {} ({}) on thread {} [{}]", + name, + closeable, + thisThread.getId(), + thisThread.getName(), + ex + ); + } } } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicLegacyCompilationIntegrationTest.java similarity index 95% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicLegacyCompilationIntegrationTest.java index 251b3e8fa..5520d9846 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicLegacyCompilationIntegrationTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.ascopes.jct.testing.integration.basic; +package io.github.ascopes.jct.testing.integration; import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; import static io.github.ascopes.jct.pathwrappers.RamFileSystem.newRamFileSystem; @@ -28,7 +28,7 @@ * @author Ashley Scopes */ @DisplayName("Basic legacy compilation integration tests") -class BasicLegacyCompilationTest { +class BasicLegacyCompilationIntegrationTest { @DisplayName("I can compile a 'Hello, World!' program") @JavacCompilerTest diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicModuleCompilationIntegrationTest.java similarity index 95% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicModuleCompilationIntegrationTest.java index 8768a07be..c0330cdce 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicModuleCompilationIntegrationTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.ascopes.jct.testing.integration.basic; +package io.github.ascopes.jct.testing.integration; import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; import static io.github.ascopes.jct.pathwrappers.RamFileSystem.newRamFileSystem; @@ -28,7 +28,7 @@ * @author Ashley Scopes */ @DisplayName("Basic module compilation integration tests") -class BasicModuleCompilationTest { +class BasicModuleCompilationIntegrationTest { @DisplayName("I can compile a 'Hello, World!' module program") @JavacCompilerTest(modules = true) diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicMultiModuleCompilationIntegrationTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicMultiModuleCompilationIntegrationTest.java index f56529cfe..44adc23ca 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/BasicMultiModuleCompilationIntegrationTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.github.ascopes.jct.testing.integration.basic; +package io.github.ascopes.jct.testing.integration; import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; import static io.github.ascopes.jct.pathwrappers.RamFileSystem.newRamFileSystem; @@ -29,7 +29,7 @@ * @author Ashley Scopes */ @DisplayName("Basic multi-module compilation integration tests") -class BasicMultiModuleCompilationTest { +class BasicMultiModuleCompilationIntegrationTest { @DisplayName("I can compile a single module using multi-module layout") @JavacCompilerTest(modules = true) diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/utils/GarbageDisposalIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/utils/GarbageDisposalIntegrationTest.java new file mode 100644 index 000000000..179099f76 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/utils/GarbageDisposalIntegrationTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2022 - 2022 Ashley Scopes + * + * Licensed 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 io.github.ascopes.jct.testing.integration.utils; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.iterable; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assumptions.abort; + +import io.github.ascopes.jct.testing.helpers.ThreadPool; +import io.github.ascopes.jct.utils.GarbageDisposal; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import org.awaitility.core.ConditionTimeoutException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Integration tests for {@link GarbageDisposal}. + * + * @author Ashley Scopes + */ +@DisplayName("GarbageDisposal integration tests") +class GarbageDisposalIntegrationTest { + + static final Logger LOGGER = LoggerFactory.getLogger(GarbageDisposalIntegrationTest.class); + + ThreadPool threadPool; + + @BeforeAll + static void ensureSystemGcHookTriggersGc() { + var initialCollections = ManagementFactory + .getGarbageCollectorMXBeans() + .stream() + .mapToLong(GarbageCollectorMXBean::getCollectionCount) + .sum(); + + System.gc(); + + try { + await("ensure System.gc() calls garbage collector immediately") + .pollInterval(10, MILLISECONDS) + .atMost(5, SECONDS) + .until(() -> initialCollections < ManagementFactory + .getGarbageCollectorMXBeans() + .stream() + .mapToLong(GarbageCollectorMXBean::getCollectionCount) + .sum()); + + } catch (ConditionTimeoutException ex) { + abort( + "Calling System.gc() did not trigger the GC hook in time, this test pack would be flaky" + ); + } + } + + @BeforeEach + @SuppressWarnings("InfiniteLoopStatement") + void setUp() { + // Set up GC stress threads. + var random = new Random(); + threadPool = new ThreadPool(1); + threadPool.execute(() -> { + LOGGER.info("Starting GC stress thread"); + // Put stress on the garbage collector to run during the tests many times. + while (true) { + var array = new int[1_000_000]; + Arrays.fill(array, random.nextInt()); + Arrays.fill(array, array[random.nextInt(array.length)]); + } + }); + } + + @AfterEach + void stopGcStress() { + LOGGER.info("Stopping GC stress thread"); + threadPool.shutdownNow(); + threadPool.close(); + } + + @DisplayName("onPhantom(Object, String, AutoCloseable) cleans up reference on garbage disposal") + @RepeatedTest(3) + void onPhantomForSingleReferenceCleansUpOnGarbageDisposal() { + // Given + var closedCount = new AtomicInteger(0); + AutoCloseable closeable = closedCount::incrementAndGet; + + // When + GarbageDisposal.onPhantom(new Object(), "foobar baz bork", closeable); + + // Then + await("the closeable object gets closed during garbage collection") + .atMost(20, SECONDS) + .pollInterval(10, MILLISECONDS) + .failFast(System::gc) + .untilAtomic(closedCount, is(greaterThanOrEqualTo(1))); + + assertThat(closedCount) + .withFailMessage("Expected exactly one closure to occur") + .hasValue(1); + } + + @DisplayName("onPhantom(Object, String, AutoCloseable) cleans up once if an exception is raised") + @RepeatedTest(3) + void onPhantomForSingleReferenceCleansUpOnceIfAnExceptionIsRaised() { + // Given + var closedCount = new AtomicInteger(0); + AutoCloseable closeable = () -> { + closedCount.incrementAndGet(); + throw new IllegalStateException("Something is wrong here, I can feel it..."); + }; + + // When + GarbageDisposal.onPhantom(new Object(), "throwing closeable", closeable); + + // Then + await("the closeable object gets closed during garbage collection") + .atMost(20, SECONDS) + .pollInterval(10, MILLISECONDS) + .failFast(System::gc) + .untilAtomic(closedCount, is(greaterThanOrEqualTo(1))); + + assertThat(closedCount) + .withFailMessage("Expected exactly one closure to occur") + .hasValue(1); + } + + @DisplayName("onPhantom(Object, Map) cleans up references on garbage disposal") + @RepeatedTest(3) + void onPhantomForMultipleReferencesCleansUpOnGarbageDisposal() { + // Given + var closeCounts = new AtomicInteger[50]; + var mapping = new HashMap(); + for (var i = 0; i < closeCounts.length; ++i) { + closeCounts[i] = new AtomicInteger(0); + mapping.put(i, closeCounts[i]::incrementAndGet); + } + + // When + GarbageDisposal.onPhantom(new Object(), mapping); + + // Then + await("the closeable objects get closed during garbage collection") + .atMost(40, SECONDS) + .pollInterval(1, MILLISECONDS) + .failFast(System::gc) + .untilAsserted(() -> assertThat(mapping) + .extracting(Map::keySet, iterable(int.class)) + .allSatisfy(index -> assertThat(closeCounts[index]).hasValueGreaterThanOrEqualTo(1))); + + assertThat(closeCounts) + .withFailMessage("Expected exactly one closure to occur in all closables") + .allSatisfy(count -> assertThat(count).hasValue(1)); + } + + @DisplayName("onPhantom(Object, Map) cleans up all resources once if an exception is raised") + @RepeatedTest(3) + void onPhantomForSingleReferenceCleansUpAllResourcesOnceIfAnExceptionIsRaised() { + + // Given + var closeCounts = new AtomicInteger[50]; + var mapping = new HashMap(); + for (var i = 0; i < closeCounts.length; ++i) { + closeCounts[i] = new AtomicInteger(0); + + // Last closeable will raise an exception in this scenario. + if (i == closeCounts.length - 1) { + var closeCount = closeCounts[i]; + mapping.put(i, () -> { + closeCount.incrementAndGet(); + throw new IllegalStateException("Something is wrong here, I can feel it..."); + }); + } else { + mapping.put(i, closeCounts[i]::incrementAndGet); + } + } + + // When + GarbageDisposal.onPhantom(new Object(), mapping); + + // Then + await("the closeable objects get closed during garbage collection") + .atMost(20, SECONDS) + .pollInterval(10, MILLISECONDS) + .failFast(System::gc) + .untilAsserted(() -> assertThat(mapping) + .extracting(Map::keySet, iterable(int.class)) + .allSatisfy(index -> assertThat(closeCounts[index]).hasValueGreaterThanOrEqualTo(1))); + + assertThat(closeCounts) + .withFailMessage("Expected exactly one closure to occur in all closables") + .allSatisfy(count -> assertThat(count).hasValue(1)); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/GarbageDisposalTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/GarbageDisposalTest.java index a5e63b526..9d838723a 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/GarbageDisposalTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/GarbageDisposalTest.java @@ -15,201 +15,20 @@ */ package io.github.ascopes.jct.testing.unit.utils; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.InstanceOfAssertFactories.iterable; -import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.core.Is.is; - import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; -import io.github.ascopes.jct.testing.helpers.ThreadPool; -import io.github.ascopes.jct.testing.helpers.ThreadPool.RunTestsInIsolation; import io.github.ascopes.jct.utils.GarbageDisposal; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Non-deterministic GCs on other platforms") +/** + * Tests for {@link GarbageDisposal}. + * + * @author Ashley Scopes + */ @DisplayName("GarbageDisposal tests") -@RunTestsInIsolation -@SuppressWarnings("InfiniteLoopStatement") class GarbageDisposalTest implements StaticClassTestTemplate { - static final Logger LOGGER = LoggerFactory.getLogger(GarbageDisposalTest.class); - - ThreadPool threadPool; - - @BeforeEach - void startGcStress() { - var random = new Random(); - threadPool = new ThreadPool(1); - threadPool.execute(() -> { - LOGGER.info("Starting GC stress thread"); - // Put stress on the garbage collector to run during the tests many times. - while (true) { - var array = new int[1_000_000]; - Arrays.fill(array, random.nextInt()); - Arrays.fill(array, array[random.nextInt(array.length)]); - } - }); - } - - @AfterEach - void stopGcStress() { - LOGGER.info("Stopping GC stress thread"); - threadPool.shutdownNow(); - threadPool.close(); - } - @Override public Class getTypeBeingTested() { return GarbageDisposal.class; } - - @DisplayName("onPhantom(Object, String, AutoCloseable) cleans up reference on garbage disposal") - @RepeatedTest(3) - void onPhantomForSingleReferenceCleansUpOnGarbageDisposal() throws InterruptedException { - // Given - var closedCount = new AtomicInteger(0); - AutoCloseable closeable = closedCount::incrementAndGet; - - // When - GarbageDisposal.onPhantom(new Object(), "foobar baz bork", closeable); - - // Then - await("the closeable object gets closed during garbage collection") - .atMost(20, SECONDS) - .pollInterval(10, MILLISECONDS) - .failFast(System::gc) - .untilAtomic(closedCount, is(greaterThanOrEqualTo(1))); - - // Wait for another GC to occur (hopefully). This will rule out the value incrementing further. - System.gc(); - Thread.sleep(250); - System.gc(); - - assertThat(closedCount) - .withFailMessage("Expected exactly one closure to occur") - .hasValue(1); - } - - @DisplayName("onPhantom(Object, String, AutoCloseable) cleans up once if an exception is raised") - @RepeatedTest(3) - void onPhantomForSingleReferenceCleansUpOnceIfAnExceptionIsRaised() throws InterruptedException { - // Given - var closedCount = new AtomicInteger(0); - AutoCloseable closeable = () -> { - closedCount.incrementAndGet(); - throw new IllegalStateException("Something is wrong here, I can feel it..."); - }; - - // When - GarbageDisposal.onPhantom(new Object(), "throwing closeable", closeable); - - // Then - await("the closeable object gets closed during garbage collection") - .atMost(20, SECONDS) - .pollInterval(10, MILLISECONDS) - .failFast(System::gc) - .untilAtomic(closedCount, is(greaterThanOrEqualTo(1))); - - // Wait for another GC to occur (hopefully). This will rule out the value incrementing further. - System.gc(); - Thread.sleep(250); - System.gc(); - - assertThat(closedCount) - .withFailMessage("Expected exactly one closure to occur") - .hasValue(1); - } - - @DisplayName("onPhantom(Object, Map) cleans up references on garbage disposal") - @RepeatedTest(3) - void onPhantomForMultipleReferencesCleansUpOnGarbageDisposal() throws InterruptedException { - // Given - var closeCounts = new AtomicInteger[50]; - var mapping = new HashMap(); - for (var i = 0; i < closeCounts.length; ++i) { - closeCounts[i] = new AtomicInteger(0); - mapping.put(i, closeCounts[i]::incrementAndGet); - } - - // When - GarbageDisposal.onPhantom(new Object(), mapping); - - // Then - await("the closeable objects get closed during garbage collection") - .atMost(20, SECONDS) - .pollInterval(10, MILLISECONDS) - .failFast(System::gc) - .untilAsserted(() -> assertThat(mapping) - .extracting(Map::keySet, iterable(int.class)) - .allSatisfy(index -> assertThat(closeCounts[index]).hasValueGreaterThanOrEqualTo(1))); - - // Wait for another GC to occur (hopefully). This will rule out the value incrementing further. - System.gc(); - Thread.sleep(250); - System.gc(); - - assertThat(closeCounts) - .withFailMessage("Expected exactly one closure to occur in all closables") - .allSatisfy(count -> assertThat(count).hasValue(1)); - } - - @DisplayName("onPhantom(Object, Map) cleans up all resources once if an exception is raised") - @RepeatedTest(3) - void onPhantomForSingleReferenceCleansUpAllResourcesOnceIfAnExceptionIsRaised() - throws InterruptedException { - - // Given - var closeCounts = new AtomicInteger[50]; - var mapping = new HashMap(); - for (var i = 0; i < closeCounts.length; ++i) { - closeCounts[i] = new AtomicInteger(0); - - // Last closeable will raise an exception in this scenario. - if (i == closeCounts.length - 1) { - var closeCount = closeCounts[i]; - mapping.put(i, () -> { - closeCount.incrementAndGet(); - throw new IllegalStateException("Something is wrong here, I can feel it..."); - }); - } else { - mapping.put(i, closeCounts[i]::incrementAndGet); - } - } - - // When - GarbageDisposal.onPhantom(new Object(), mapping); - - // Then - await("the closeable objects get closed during garbage collection") - .atMost(20, SECONDS) - .pollInterval(10, MILLISECONDS) - .failFast(System::gc) - .untilAsserted(() -> assertThat(mapping) - .extracting(Map::keySet, iterable(int.class)) - .allSatisfy(index -> assertThat(closeCounts[index]).hasValueGreaterThanOrEqualTo(1))); - - // Wait for another GC to occur (hopefully). This will rule out the value incrementing further. - System.gc(); - Thread.sleep(250); - System.gc(); - - assertThat(closeCounts) - .withFailMessage("Expected exactly one closure to occur in all closables") - .allSatisfy(count -> assertThat(count).hasValue(1)); - } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java index e0bbedc47..1b632f108 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java @@ -88,7 +88,7 @@ void accessReturnsTheCachedResult() { @ConcurrentRuns @ParameterizedTest(name = "for {0} concurrent read(s)") @RunTestsInIsolation - @Timeout(10) + @Timeout(30) void accessSynchronizesCorrectly(int concurrency) { try (var executor = new ThreadPool(concurrency)) { // Given