TestAutoCloseables.java

/*
 * 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.arrow.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.junit.jupiter.api.Test;

public class TestAutoCloseables {

  /** Closeable that records that it was closed and can optionally throw. */
  private static final class TrackCloseable implements AutoCloseable {
    private boolean closed;
    private final Exception toThrow;

    TrackCloseable() {
      this.toThrow = null;
    }

    TrackCloseable(Exception toThrow) {
      this.toThrow = toThrow;
    }

    @Override
    public void close() throws Exception {
      closed = true;
      if (toThrow != null) {
        throw toThrow;
      }
    }

    boolean isClosed() {
      return closed;
    }
  }

  @Test
  public void testCloseVarargsIgnoresNulls() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    AutoCloseables.close(a, null, b);
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
  }

  @Test
  public void testCloseVarargsThrowsFirstExceptionAndSuppressesRest() throws Exception {
    Exception e1 = new Exception("first");
    Exception e2 = new Exception("second");
    TrackCloseable c1 = new TrackCloseable(e1);
    TrackCloseable c2 = new TrackCloseable(e2);
    Exception thrown = assertThrows(Exception.class, () -> AutoCloseables.close(c1, c2));
    assertEquals("first", thrown.getMessage());
    assertTrue(Arrays.asList(thrown.getSuppressed()).contains(e2));
  }

  @Test
  public void testCloseIterableNullIterableReturns() throws Exception {
    AutoCloseables.close((List<AutoCloseable>) null); // no exception
  }

  @Test
  public void testCloseIterableIgnoresNullElements() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    List<AutoCloseable> list = Arrays.asList(a, null, b);
    AutoCloseables.close(list);
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
  }

  @Test
  public void testCloseIterableWhenIterableIsAlsoAutoCloseable() throws Exception {
    TrackCloseable iter = new TrackCloseable();
    TrackCloseable inner = new TrackCloseable();
    // When the Iterable itself implements AutoCloseable (e.g. VectorContainer),
    // close(Iterable) calls close() on it and does not iterate over elements
    class IterableCloseable implements Iterable<AutoCloseable>, AutoCloseable {
      @Override
      @SuppressWarnings("unchecked")
      public Iterator<AutoCloseable> iterator() {
        return (Iterator<AutoCloseable>) Collections.singletonList(inner);
      }

      @Override
      public void close() throws Exception {
        iter.close();
      }
    }
    AutoCloseables.close(new IterableCloseable());
    assertTrue(iter.isClosed());
    assertFalse(inner.isClosed());
  }

  @Test
  public void testCloseIterableVarargsWithNullIterables() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    TrackCloseable c = new TrackCloseable();
    List<AutoCloseable> list1 = Arrays.asList(null, a, b);
    List<AutoCloseable> list2 = Collections.singletonList(c);
    AutoCloseables.close(list1, null, list2);
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
    assertTrue(c.isClosed());
  }

  @Test
  public void testCloseThrowableSuppressesException() {
    Exception e = new Exception("from close");
    TrackCloseable c = new TrackCloseable(e);
    Exception main = new Exception("main");
    AutoCloseables.close(main, c);
    assertTrue(c.isClosed());
    assertEquals(1, main.getSuppressed().length);
    assertEquals(e, main.getSuppressed()[0]);
  }

  @Test
  public void testCloseThrowableWithNullCloseables() {
    Exception main = new Exception("main");
    AutoCloseables.close(main, (AutoCloseable) null);
    assertEquals(0, main.getSuppressed().length);

    AutoCloseables.close(main, (AutoCloseable[]) null); // no exception
  }

  @Test
  public void testIterFiltersNulls() {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    Iterable<AutoCloseable> it = AutoCloseables.iter(a, null, b);
    List<AutoCloseable> list = new ArrayList<>();
    it.forEach(list::add);
    assertEquals(2, list.size());
    assertTrue(list.contains(a));
    assertTrue(list.contains(b));
  }

  @Test
  public void testIterEmptyVarargs() {
    Iterable<AutoCloseable> it = AutoCloseables.iter();
    List<AutoCloseable> list = new ArrayList<>();
    it.forEach(list::add);
    assertTrue(list.isEmpty());
  }

  @Test
  public void testIterWithNull() {
    AutoCloseables.iter((AutoCloseable) null); // no exception
  }

  @Test
  public void testCloseNoCheckedWithNull() {
    AutoCloseables.closeNoChecked(null); // no exception
  }

  @Test
  public void testCloseNoCheckedWrapsException() {
    Exception e = new Exception("close failed");
    TrackCloseable c = new TrackCloseable(e);
    RuntimeException re =
        assertThrows(RuntimeException.class, () -> AutoCloseables.closeNoChecked(c));
    assertSame(re.getCause(), e);
    assertTrue(re.getMessage().contains("close failed"));
  }

  @Test
  public void testNoop() throws Exception {
    AutoCloseable noop = AutoCloseables.noop();
    assertSame(noop, AutoCloseables.noop());
    noop.close(); // no exception
  }

  @Test
  public void testAllClosesCollectionOnClose() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    List<AutoCloseable> list = Arrays.asList(a, b);
    AutoCloseable all = AutoCloseables.all(list);
    assertFalse(a.isClosed());
    assertFalse(b.isClosed());
    all.close();
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
  }

  @Test
  public void testAllWithNullCollection() throws Exception {
    AutoCloseable all = AutoCloseables.all(null);
    all.close(); // no exception
  }

  @Test
  public void testRollbackCloseableClosesWhenNotCommitted() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    AutoCloseables.RollbackCloseable rb = AutoCloseables.rollbackable(a, b);
    rb.close();
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
  }

  @Test
  public void testRollbackCloseableDoesNotCloseWhenCommitted() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    AutoCloseables.RollbackCloseable rb = AutoCloseables.rollbackable(a, b);
    rb.commit();
    rb.close();
    assertFalse(a.isClosed());
    assertFalse(b.isClosed());
  }

  @Test
  public void testRollbackCloseableAddAndAddAll() throws Exception {
    TrackCloseable a = new TrackCloseable();
    TrackCloseable b = new TrackCloseable();
    TrackCloseable c = new TrackCloseable();
    TrackCloseable d = new TrackCloseable();
    AutoCloseables.RollbackCloseable rb = AutoCloseables.rollbackable(a);
    rb.add(b);
    rb.addAll(c, d);
    rb.addAll((AutoCloseable[]) null); // null varargs shouldn't fail
    rb.addAll((List<AutoCloseable>) null); // null Iterable shouldn't fail
    rb.close();
    assertTrue(a.isClosed());
    assertTrue(b.isClosed());
    assertTrue(c.isClosed());
    assertTrue(d.isClosed());
  }

  @Test
  public void testRollbackCloseableWithNull() throws Exception {
    AutoCloseables.rollbackable((AutoCloseable) null); // no exception
  }

  @Test
  public void testRollbackCloseableWithNulls() throws Exception {
    TrackCloseable a = new TrackCloseable();
    AutoCloseables.RollbackCloseable rb = AutoCloseables.rollbackable(a, null);
    rb.close();
    assertTrue(a.isClosed());
  }
}