001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.mapreduce;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.mockito.ArgumentMatchers.any;
022import static org.mockito.ArgumentMatchers.eq;
023import static org.mockito.Mockito.doNothing;
024import static org.mockito.Mockito.verify;
025
026import java.io.IOException;
027import java.util.Collection;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import org.apache.hadoop.conf.Configuration;
032import org.apache.hadoop.fs.Path;
033import org.apache.hadoop.hbase.client.Scan;
034import org.apache.hadoop.hbase.testclassification.SmallTests;
035import org.apache.hadoop.hbase.util.Bytes;
036import org.apache.hadoop.hbase.util.CommonFSUtils;
037import org.junit.jupiter.api.BeforeEach;
038import org.junit.jupiter.api.Tag;
039import org.junit.jupiter.api.Test;
040import org.mockito.Mockito;
041
042import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableList;
043import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
044import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
045import org.apache.hbase.thirdparty.com.google.common.collect.Maps;
046
047@Tag(SmallTests.TAG)
048public class TestMultiTableSnapshotInputFormatImpl {
049
050  private MultiTableSnapshotInputFormatImpl subject;
051  private Map<String, Collection<Scan>> snapshotScans;
052  private Path restoreDir;
053  private Configuration conf;
054  private Path rootDir;
055
056  @BeforeEach
057  public void setUp() throws Exception {
058    this.subject = Mockito.spy(new MultiTableSnapshotInputFormatImpl());
059
060    // mock out restoreSnapshot
061    // TODO: this is kind of meh; it'd be much nicer to just inject the RestoreSnapshotHelper
062    // dependency into the
063    // input format. However, we need a new RestoreSnapshotHelper per snapshot in the current
064    // design, and it *also*
065    // feels weird to introduce a RestoreSnapshotHelperFactory and inject that, which would
066    // probably be the more "pure"
067    // way of doing things. This is the lesser of two evils, perhaps?
068    doNothing().when(this.subject).restoreSnapshot(any(), any(), any(), any(), any());
069
070    this.conf = new Configuration();
071    this.rootDir = new Path("file:///test-root-dir");
072    CommonFSUtils.setRootDir(conf, rootDir);
073    this.snapshotScans = ImmutableMap.<String, Collection<Scan>> of("snapshot1",
074      ImmutableList.of(new Scan().withStartRow(Bytes.toBytes("1")).withStopRow(Bytes.toBytes("2"))),
075      "snapshot2",
076      ImmutableList.of(new Scan().withStartRow(Bytes.toBytes("3")).withStopRow(Bytes.toBytes("4")),
077        new Scan().withStartRow(Bytes.toBytes("5")).withStopRow(Bytes.toBytes("6"))));
078
079    this.restoreDir = new Path(CommonFSUtils.getRootDir(conf), "restore-dir");
080
081  }
082
083  public void callSetInput() throws IOException {
084    subject.setInput(this.conf, snapshotScans, restoreDir);
085  }
086
087  public Map<String, Collection<ScanWithEquals>>
088    toScanWithEquals(Map<String, Collection<Scan>> snapshotScans) throws IOException {
089    Map<String, Collection<ScanWithEquals>> rtn = Maps.newHashMap();
090
091    for (Map.Entry<String, Collection<Scan>> entry : snapshotScans.entrySet()) {
092      List<ScanWithEquals> scans = Lists.newArrayList();
093
094      for (Scan scan : entry.getValue()) {
095        scans.add(new ScanWithEquals(scan));
096      }
097      rtn.put(entry.getKey(), scans);
098    }
099
100    return rtn;
101  }
102
103  public static class ScanWithEquals {
104
105    private final String startRow;
106    private final String stopRow;
107
108    /**
109     * Creates a new instance of this class while copying all values.
110     * @param scan The scan instance to copy from.
111     * @throws java.io.IOException When copying the values fails.
112     */
113    public ScanWithEquals(Scan scan) throws IOException {
114      this.startRow = Bytes.toStringBinary(scan.getStartRow());
115      this.stopRow = Bytes.toStringBinary(scan.getStopRow());
116    }
117
118    @Override
119    public boolean equals(Object obj) {
120      if (!(obj instanceof ScanWithEquals)) {
121        return false;
122      }
123      ScanWithEquals otherScan = (ScanWithEquals) obj;
124      return Objects.equals(this.startRow, otherScan.startRow)
125        && Objects.equals(this.stopRow, otherScan.stopRow);
126    }
127
128    @Override
129    public int hashCode() {
130      return Objects.hash(startRow, stopRow);
131    }
132
133    @Override
134    public String toString() {
135      return org.apache.hbase.thirdparty.com.google.common.base.MoreObjects.toStringHelper(this)
136        .add("startRow", startRow).add("stopRow", stopRow).toString();
137    }
138  }
139
140  @Test
141  public void testSetInputSetsSnapshotToScans() throws Exception {
142
143    callSetInput();
144
145    Map<String, Collection<Scan>> actual = subject.getSnapshotsToScans(conf);
146
147    // convert to scans we can use .equals on
148    Map<String, Collection<ScanWithEquals>> actualWithEquals = toScanWithEquals(actual);
149    Map<String, Collection<ScanWithEquals>> expectedWithEquals = toScanWithEquals(snapshotScans);
150
151    assertEquals(expectedWithEquals, actualWithEquals);
152  }
153
154  @Test
155  public void testSetInputPushesRestoreDirectories() throws Exception {
156    callSetInput();
157
158    Map<String, Path> restoreDirs = subject.getSnapshotDirs(conf);
159
160    assertEquals(this.snapshotScans.keySet(), restoreDirs.keySet());
161  }
162
163  @Test
164  public void testSetInputCreatesRestoreDirectoriesUnderRootRestoreDir() throws Exception {
165    callSetInput();
166
167    Map<String, Path> restoreDirs = subject.getSnapshotDirs(conf);
168
169    for (Path snapshotDir : restoreDirs.values()) {
170      assertEquals(restoreDir, snapshotDir.getParent(),
171        "Expected " + snapshotDir + " to be a child of " + restoreDir);
172    }
173  }
174
175  @Test
176  public void testSetInputRestoresSnapshots() throws Exception {
177    callSetInput();
178
179    Map<String, Path> snapshotDirs = subject.getSnapshotDirs(conf);
180
181    for (Map.Entry<String, Path> entry : snapshotDirs.entrySet()) {
182      verify(this.subject).restoreSnapshot(eq(this.conf), eq(entry.getKey()), eq(this.rootDir),
183        eq(entry.getValue()), any());
184    }
185  }
186}