diff --git a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java index 02d97976b35..b4a11c8084f 100644 --- a/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java +++ b/core/src/main/java/org/apache/accumulo/core/metadata/schema/ExternalCompactionMetadata.java @@ -54,6 +54,12 @@ public ExternalCompactionMetadata(Set jobFiles, Set extCompactionsToRemove = new HashMap<>(); - var extSelInfo = initExternalSelection(extCompactions, tablet, extCompactionsToRemove); + // Memoize the supplier so it only calls tablet.getCompactionID() once, because the impl goes to + // zookeeper. It's a supplier because it may not be needed. + Supplier>> tabletCompactionId = Suppliers.memoize(() -> { + try { + return Optional.of(tablet.getCompactionID()); + } catch (NoNodeException nne) { + return Optional.empty(); + } + }); - verifyExternalCompactions(extCompactions, dataFileSizes.keySet(), extCompactionsToRemove); + var extSelInfo = + processExternalMetadata(extCompactions, () -> tabletCompactionId.get().map(Pair::getFirst), + dataFileSizes.keySet(), extCompactionsToRemove); + + if (extSelInfo.isPresent()) { + if (extSelInfo.get().selectKind == CompactionKind.USER) { + this.chelper = CompactableUtils.getHelper(extSelInfo.get().selectKind, tablet, + tabletCompactionId.get().get().getFirst(), tabletCompactionId.get().get().getSecond()); + this.compactionConfig = tabletCompactionId.get().get().getSecond(); + this.compactionId = tabletCompactionId.get().get().getFirst(); + } else if (extSelInfo.get().selectKind == CompactionKind.SELECTOR) { + this.chelper = CompactableUtils.getHelper(extSelInfo.get().selectKind, tablet, null, null); + } + } extCompactionsToRemove.forEach((ecid, reason) -> { log.warn("Removing external compaction {} for {} because {} meta: {}", ecid, @@ -669,30 +690,6 @@ protected long getNanoTime() { }; } - private void verifyExternalCompactions( - Map extCompactions, - Set tabletFiles, Map extCompactionsToRemove) { - - Set seen = new HashSet<>(); - boolean overlap = false; - - for (var entry : extCompactions.entrySet()) { - ExternalCompactionMetadata ecMeta = entry.getValue(); - if (!tabletFiles.containsAll(ecMeta.getJobFiles())) { - extCompactionsToRemove.putIfAbsent(entry.getKey(), "Has files outside of tablet files"); - } else if (!Collections.disjoint(seen, ecMeta.getJobFiles())) { - overlap = true; - } - seen.addAll(ecMeta.getJobFiles()); - } - - if (overlap) { - extCompactions.keySet().forEach(ecid -> { - extCompactionsToRemove.putIfAbsent(ecid, "Some external compaction files overlap"); - }); - } - } - private synchronized boolean addJob(CompactionJob job) { if (runningJobs.add(job)) { compactionRunning = true; @@ -815,16 +812,47 @@ private void checkIfUserCompactionCanceled() { } /** - * For user compactions a set of files is selected. Those files then get compacted by one or more - * compactions until the set is empty. This method attempts to reconstruct the selected set of - * files when a tablet is loaded with an external user compaction. It avoids repeating work and - * when a user compaction completes, files are verified against the selected set. Since the data - * is coming from persisted storage, lots of checks are done in this method rather than assuming - * the persisted data is correct. + * This method validates metadata about external compactions. It also extracts specific + * information needed for user and selector compactions. */ - private Optional initExternalSelection( - Map extCompactions, Tablet tablet, + static Optional processExternalMetadata( + Map extCompactions, + Supplier> tabletCompactionId, Set tabletFiles, Map externalCompactionsToRemove) { + + // Check that external compactions have disjoint sets of files. Also check that each external + // compaction only has files inside a tablet. + Set seen = new HashSet<>(); + boolean overlap = false; + + for (var entry : extCompactions.entrySet()) { + ExternalCompactionMetadata ecMeta = entry.getValue(); + if (!tabletFiles.containsAll(ecMeta.getJobFiles())) { + externalCompactionsToRemove.putIfAbsent(entry.getKey(), + "Has files outside of tablet files"); + } else if (!Collections.disjoint(seen, ecMeta.getJobFiles())) { + overlap = true; + } + seen.addAll(ecMeta.getJobFiles()); + } + + if (overlap) { + extCompactions.keySet().forEach(ecid -> { + externalCompactionsToRemove.putIfAbsent(ecid, "Some external compaction files overlap"); + }); + return Optional.empty(); + } + + /* + * The rest of the code validates user compaction metadata and extracts needed information. + * + * For user compactions a set of files is selected. Those files then get compacted by one or + * more compactions until the set is empty. This method attempts to reconstruct the selected set + * of files when a tablet is loaded with an external user compaction. It avoids repeating work + * and when a user compaction completes, files are verified against the selected set. Since the + * data is coming from persisted storage, lots of checks are done in this method rather than + * assuming the persisted data is correct. + */ CompactionKind extKind = null; boolean unexpectedExternal = false; Set tmpSelectedFiles = null; @@ -902,17 +930,14 @@ private Optional initExternalSelection( reasons.add("Concurrent compactions not propagatingDeletes"); } - Pair idAndCfg = null; if (extKind == CompactionKind.USER) { - try { - idAndCfg = tablet.getCompactionID(); - if (!idAndCfg.getFirst().equals(cid)) { - unexpectedExternal = true; - reasons.add("Compaction id mismatch with zookeeper"); - } - } catch (NoNodeException e) { + Optional compactionId = tabletCompactionId.get(); + if (compactionId.isEmpty()) { unexpectedExternal = true; reasons.add("No compaction id in zookeeper"); + } else if (!compactionId.get().equals(cid)) { + unexpectedExternal = true; + reasons.add("Compaction id mismatch with zookeeper"); } } @@ -925,17 +950,8 @@ private Optional initExternalSelection( return Optional.empty(); } - if (extKind != null) { - if (extKind == CompactionKind.USER) { - this.chelper = CompactableUtils.getHelper(extKind, tablet, cid, idAndCfg.getSecond()); - this.compactionConfig = idAndCfg.getSecond(); - this.compactionId = cid; - } else if (extKind == CompactionKind.SELECTOR) { - this.chelper = CompactableUtils.getHelper(extKind, tablet, null, null); - } - + if (extKind != null) return Optional.of(new SelectedInfo(initiallySelAll, tmpSelectedFiles, extKind)); - } return Optional.empty(); } diff --git a/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplFileManagerTest.java b/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplFileManagerTest.java index 42e934f88c6..ccf84a3707f 100644 --- a/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplFileManagerTest.java +++ b/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplFileManagerTest.java @@ -448,11 +448,11 @@ Set getCompactingFiles() { } - private static StoredTabletFile newFile(String f) { + static StoredTabletFile newFile(String f) { return new StoredTabletFile("hdfs://nn1/accumulo/tables/1/t-0001/" + f); } - private static Set newFiles(String... strings) { + static Set newFiles(String... strings) { return Arrays.asList(strings).stream().map(s -> newFile(s)).collect(Collectors.toSet()); } diff --git a/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplTest.java b/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplTest.java new file mode 100644 index 00000000000..983145a83cf --- /dev/null +++ b/server/tserver/src/test/java/org/apache/accumulo/tserver/tablet/CompactableImplTest.java @@ -0,0 +1,396 @@ +/* + * 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.accumulo.tserver.tablet; + +import static org.apache.accumulo.tserver.tablet.CompactableImplFileManagerTest.newFile; +import static org.apache.accumulo.tserver.tablet.CompactableImplFileManagerTest.newFiles; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.apache.accumulo.core.metadata.StoredTabletFile; +import org.apache.accumulo.core.metadata.TabletFile; +import org.apache.accumulo.core.metadata.schema.ExternalCompactionId; +import org.apache.accumulo.core.metadata.schema.ExternalCompactionMetadata; +import org.apache.accumulo.core.spi.compaction.CompactionExecutorId; +import org.apache.accumulo.core.spi.compaction.CompactionKind; +import org.apache.accumulo.core.util.compaction.CompactionExecutorIdImpl; +import org.junit.Test; + +public class CompactableImplTest { + private static ExternalCompactionMetadata newECM(Set jobFiles, + Set nextFiles, CompactionKind kind, boolean propagateDeletes, + boolean initiallySelectedAll) { + + return newECM(jobFiles, nextFiles, kind, propagateDeletes, initiallySelectedAll, 5L); + } + + private static ExternalCompactionMetadata newECM(Set jobFiles, + Set nextFiles, CompactionKind kind, boolean propagateDeletes, + boolean initiallySelectedAll, Long compactionId) { + + TabletFile compactTmpName = newFile("C00000A.rf_tmp"); + String compactorId = "cid"; + short priority = 9; + CompactionExecutorId ceid = CompactionExecutorIdImpl.externalId("ecs1"); + + return new ExternalCompactionMetadata(jobFiles, nextFiles, compactTmpName, compactorId, kind, + priority, ceid, propagateDeletes, initiallySelectedAll, compactionId); + } + + ExternalCompactionId newEcid() { + return ExternalCompactionId.generate(UUID.randomUUID()); + } + + @Test + public void testExternalOverlapping() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, Set.of(), CompactionKind.SYSTEM, true, false); + + var fileSet2 = newFiles("F00002", "F00003"); + var ecm2 = newECM(fileSet2, Set.of(), CompactionKind.USER, true, false); + + var fileSet3 = newFiles("F00004", "F00005"); + var ecm3 = newECM(fileSet3, Set.of(), CompactionKind.SYSTEM, true, false); + + Map toRemove = new HashMap<>(); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + var ecid3 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2, ecid3, ecm3); + var selInfo = CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.empty(), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005"), toRemove); + + // The external compaction metadata contains compactions with overlapping jobs. The code + // currently marks everything for remove when this happens. So even though only ecid1 and ecid2 + // overlap, ecid3 is also flagged for removal. + assertEquals(Set.of(ecid1, ecid2, ecid3), toRemove.keySet()); + assertTrue(toRemove.values().stream() + .allMatch(v -> v.contains("Some external compaction files overlap"))); + assertTrue(selInfo.isEmpty()); + } + + @Test + public void testExternalFilesOutsideTablet() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, Set.of(), CompactionKind.SYSTEM, true, false); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, Set.of(), CompactionKind.USER, true, false); + + var fileSet3 = newFiles("F00005", "F00006"); + var ecm3 = newECM(fileSet3, Set.of(), CompactionKind.SYSTEM, true, false); + + Map toRemove = new HashMap<>(); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + var ecid3 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2, ecid3, ecm3); + var selInfo = CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.empty(), + newFiles("F00004", "F00005", "F00006"), toRemove); + + // The external compaction ids ecid1 and ecid2 are related to files that are not in the tablet + // files, so that external compaction metadata should be removed + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("Has files outside of tablet files"))); + assertTrue(selInfo.isEmpty()); + } + + @Test + public void testExternalUserDifferentSelectedFiles() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, false); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.USER, true, false); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + // the two external compactions should compute the same set of selected files, so neither should + // be removed + assertEquals(Set.of(), toRemove.keySet()); + + var fileSet3 = newFiles("F00003", "F00004"); + var ecm3 = + newECM(fileSet3, newFiles("F00001", "F00002", "F00005"), CompactionKind.USER, true, false); + + extCompactions = Map.of(ecid1, ecm1, ecid2, ecm3); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005"), toRemove); + + // A different set of selected files is computed for the two external compactions, so both + // should be ignored and removed + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("Selected set of files differs"))); + + var fileSet4 = newFiles("F00003", "F00004"); + var ecm4 = newECM(fileSet4, newFiles("F00001"), CompactionKind.USER, true, false); + + extCompactions = Map.of(ecid1, ecm1, ecid2, ecm4); + toRemove.clear(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005"), toRemove); + + // A different set of selected files is computed for the two external compactions, so both + // should be ignored and removed + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("Selected set of files differs"))); + + } + + @Test + public void testDifferingCompactionId() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, false, 5L); + var ecid1 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(4L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1), toRemove.keySet()); + assertTrue(toRemove.values().stream() + .allMatch(v -> v.contains("Compaction id mismatch with zookeeper"))); + + ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, false, null); + + extCompactions = Map.of(ecid1, ecm1); + + toRemove.clear(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(4L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1), toRemove.keySet()); + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("Compaction id mismatch with zookeeper") + && v.contains("Missing compactionId"))); + + } + + @Test + public void testSelectedAllDisagreement() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, true); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.USER, true, false); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + assertTrue(toRemove.values().stream().allMatch(v -> v.contains("Disagreement on selectedAll"))); + } + + @Test + public void testConcurrentPropDels() { + // For concurrent user compactions they should always propagate deletes. Only the last single + // user compaction can not propogate deletes. + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, false, true); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.USER, false, true); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + + assertTrue(toRemove.values().stream() + .allMatch(v -> v.contains("Concurrent compactions not propagatingDeletes"))); + } + + @Test + public void testPropDelDisagreement() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, false, true); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.USER, true, true); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("Disagreement on propagateDeletes"))); + } + + @Test + public void testNoComactionIdInZookeeper() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, false, 5L); + var ecid1 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.empty(), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1), toRemove.keySet()); + + assertTrue( + toRemove.values().stream().allMatch(v -> v.contains("No compaction id in zookeeper"))); + } + + @Test + public void testDifferentKinds() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, true); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = + newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.SELECTOR, true, true, null); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1, ecid2), toRemove.keySet()); + + assertTrue(toRemove.values().stream().allMatch(v -> v.contains("Saw USER and SELECTOR"))); + } + + @Test + public void testSelectorWithCompactionId() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = + newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.SELECTOR, true, true, 5L); + + var ecid1 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1); + + Map toRemove = new HashMap<>(); + + CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004"), toRemove); + + assertEquals(Set.of(ecid1), toRemove.keySet()); + + assertTrue(toRemove.values().stream().allMatch(v -> v.contains("Unexpected compactionId"))); + } + + @Test + public void testNominalExternalCompactionMetadata() { + var fileSet1 = newFiles("F00001", "F00002"); + var ecm1 = newECM(fileSet1, newFiles("F00003", "F00004"), CompactionKind.USER, true, true); + + var fileSet2 = newFiles("F00003", "F00004"); + var ecm2 = newECM(fileSet2, newFiles("F00001", "F00002"), CompactionKind.USER, true, true); + + var fileSet3 = newFiles("F00005", "F00006"); + var ecm3 = newECM(fileSet3, Set.of(), CompactionKind.SYSTEM, true, false); + + var ecid1 = newEcid(); + var ecid2 = newEcid(); + var ecid3 = newEcid(); + + var extCompactions = Map.of(ecid1, ecm1, ecid2, ecm2, ecid3, ecm3); + + Map toRemove = new HashMap<>(); + + // This test checks the case of when there is nothing unexpected in the ext compaction metadata + // and then verifies the return values are as expected. + + var selInfoOpt = CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005", "F00006"), toRemove); + assertEquals(Set.of(), toRemove.keySet()); + assertTrue(selInfoOpt.isPresent()); + var selInfo = selInfoOpt.get(); + + assertTrue(selInfo.initiallySelectedAll); + assertEquals(CompactionKind.USER, selInfo.selectKind); + assertEquals(newFiles("F00001", "F00002", "F00003", "F00004"), selInfo.selectedFiles); + + // Try processing only system compactions. + extCompactions = Map.of(ecid3, ecm3); + selInfoOpt = CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005", "F00006"), toRemove); + assertTrue(selInfoOpt.isEmpty()); + assertEquals(Map.of(), toRemove); + + // Try a single user compaction that does not propagate deletes. + ecm1 = newECM(newFiles("F00001", "F00002", "F00003", "F00004"), Set.of(), CompactionKind.USER, + false, true); + selInfoOpt = CompactableImpl.processExternalMetadata(extCompactions, () -> Optional.of(5L), + newFiles("F00001", "F00002", "F00003", "F00004", "F00005", "F00006"), toRemove); + assertTrue(selInfo.initiallySelectedAll); + assertEquals(CompactionKind.USER, selInfo.selectKind); + assertEquals(newFiles("F00001", "F00002", "F00003", "F00004"), selInfo.selectedFiles); + } + +}