package org.apache.lucene.store; /** * 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. */ import java.io.Closeable; import java.io.IOException; import java.io.FileNotFoundException; import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.Random; import java.util.Map; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.ArrayList; /** * This is a Directory Wrapper that adds methods * intended to be used only by unit tests. */ public class MockDirectoryWrapper extends Directory { final Directory delegate; long maxSize; // Max actual bytes used. This is set by MockRAMOutputStream: long maxUsedSize; double randomIOExceptionRate; Random randomState; boolean noDeleteOpenFile = true; boolean preventDoubleWrite = true; boolean trackDiskUsage = false; private Set<String> unSyncedFiles; private Set<String> createdFiles; volatile boolean crashed; // use this for tracking files for crash. // additionally: provides debugging information in case you leave one open Map<Closeable,Exception> files = Collections.synchronizedMap(new IdentityHashMap<Closeable,Exception>()); // NOTE: we cannot initialize the Map here due to the // order in which our constructor actually does this // member initialization vs when it calls super. It seems // like super is called, then our members are initialized: Map<String,Integer> openFiles; private synchronized void init() { if (openFiles == null) openFiles = new HashMap<String,Integer>(); if (createdFiles == null) createdFiles = new HashSet<String>(); if (unSyncedFiles == null) unSyncedFiles = new HashSet<String>(); } public MockDirectoryWrapper(Directory delegate) { this.delegate = delegate; init(); } public void setTrackDiskUsage(boolean v) { trackDiskUsage = v; } /** If set to true, we throw an IOException if the same * file is opened by createOutput, ever. */ public void setPreventDoubleWrite(boolean value) { preventDoubleWrite = value; } @Deprecated @Override public void sync(String name) throws IOException { maybeThrowDeterministicException(); if (crashed) throw new IOException("cannot sync after crash"); unSyncedFiles.remove(name); delegate.sync(name); } @Override public synchronized void sync(Collection<String> names) throws IOException { for (String name : names) maybeThrowDeterministicException(); if (crashed) throw new IOException("cannot sync after crash"); unSyncedFiles.removeAll(names); delegate.sync(names); } @Override public String toString() { return "MockDirWrapper(" + delegate + ")"; } public synchronized final long sizeInBytes() throws IOException { if (delegate instanceof RAMDirectory) return ((RAMDirectory) delegate).sizeInBytes(); else { // hack long size = 0; for (String file : delegate.listAll()) size += delegate.fileLength(file); return size; } } /** Simulates a crash of OS or machine by overwriting * unsynced files. */ public synchronized void crash() throws IOException { crashed = true; openFiles = new HashMap<String,Integer>(); Iterator<String> it = unSyncedFiles.iterator(); unSyncedFiles = new HashSet<String>(); // first force-close all files, so we can corrupt on windows etc. // clone the file map, as these guys want to remove themselves on close. Map<Closeable,Exception> m = new IdentityHashMap<Closeable,Exception>(files); for (Closeable f : m.keySet()) try { f.close(); } catch (Exception ignored) {} int count = 0; while(it.hasNext()) { String name = it.next(); if (count % 3 == 0) { deleteFile(name, true); } else if (count % 3 == 1) { // Zero out file entirely long length = fileLength(name); byte[] zeroes = new byte[256]; long upto = 0; IndexOutput out = delegate.createOutput(name); while(upto < length) { final int limit = (int) Math.min(length-upto, zeroes.length); out.writeBytes(zeroes, 0, limit); upto += limit; } out.close(); } else if (count % 3 == 2) { // Truncate the file: IndexOutput out = delegate.createOutput(name); out.setLength(fileLength(name)/2); out.close(); } count++; } } public synchronized void clearCrash() throws IOException { crashed = false; } public void setMaxSizeInBytes(long maxSize) { this.maxSize = maxSize; } public long getMaxSizeInBytes() { return this.maxSize; } /** * Returns the peek actual storage used (bytes) in this * directory. */ public long getMaxUsedSizeInBytes() { return this.maxUsedSize; } public void resetMaxUsedSizeInBytes() throws IOException { this.maxUsedSize = getRecomputedActualSizeInBytes(); } /** * Emulate windows whereby deleting an open file is not * allowed (raise IOException). */ public void setNoDeleteOpenFile(boolean value) { this.noDeleteOpenFile = value; } public boolean getNoDeleteOpenFile() { return noDeleteOpenFile; } /** * If 0.0, no exceptions will be thrown. Else this should * be a double 0.0 - 1.0. We will randomly throw an * IOException on the first write to an OutputStream based * on this probability. */ public void setRandomIOExceptionRate(double rate, long seed) { randomIOExceptionRate = rate; // seed so we have deterministic behaviour: randomState = new Random(seed); } public double getRandomIOExceptionRate() { return randomIOExceptionRate; } void maybeThrowIOException() throws IOException { if (randomIOExceptionRate > 0.0) { int number = Math.abs(randomState.nextInt() % 1000); if (number < randomIOExceptionRate*1000) { throw new IOException("a random IOException"); } } } @Override public synchronized void deleteFile(String name) throws IOException { deleteFile(name, false); } private synchronized void deleteFile(String name, boolean forced) throws IOException { maybeThrowDeterministicException(); if (crashed && !forced) throw new IOException("cannot delete after crash"); if (unSyncedFiles.contains(name)) unSyncedFiles.remove(name); if (!forced) { if (noDeleteOpenFile && openFiles.containsKey(name)) { throw new IOException("MockDirectoryWrapper: file \"" + name + "\" is still open: cannot delete"); } } delegate.deleteFile(name); } @Override public synchronized IndexOutput createOutput(String name) throws IOException { if (crashed) throw new IOException("cannot createOutput after crash"); init(); synchronized(this) { if (preventDoubleWrite && createdFiles.contains(name) && !name.equals("segments.gen")) throw new IOException("file \"" + name + "\" was already written to"); } if (noDeleteOpenFile && openFiles.containsKey(name)) throw new IOException("MockDirectoryWrapper: file \"" + name + "\" is still open: cannot overwrite"); if (crashed) throw new IOException("cannot createOutput after crash"); unSyncedFiles.add(name); createdFiles.add(name); if (delegate instanceof RAMDirectory) { RAMDirectory ramdir = (RAMDirectory) delegate; RAMFile file = new RAMFile(ramdir); RAMFile existing = ramdir.fileMap.get(name); // Enforce write once: if (existing!=null && !name.equals("segments.gen") && preventDoubleWrite) throw new IOException("file " + name + " already exists"); else { if (existing!=null) { ramdir.sizeInBytes.getAndAdd(-existing.sizeInBytes); existing.directory = null; } ramdir.fileMap.put(name, file); } } IndexOutput io = new MockIndexOutputWrapper(this, delegate.createOutput(name), name); files.put(io, new RuntimeException("unclosed IndexOutput")); return io; } @Override public synchronized IndexInput openInput(String name) throws IOException { if (!delegate.fileExists(name)) throw new FileNotFoundException(name); else { if (openFiles.containsKey(name)) { Integer v = openFiles.get(name); v = Integer.valueOf(v.intValue()+1); openFiles.put(name, v); } else { openFiles.put(name, Integer.valueOf(1)); } } IndexInput ii = new MockIndexInputWrapper(this, name, delegate.openInput(name)); files.put(ii, new RuntimeException("unclosed IndexInput")); return ii; } /** Provided for testing purposes. Use sizeInBytes() instead. */ public synchronized final long getRecomputedSizeInBytes() throws IOException { if (!(delegate instanceof RAMDirectory)) return sizeInBytes(); long size = 0; for(final RAMFile file: ((RAMDirectory)delegate).fileMap.values()) { size += file.getSizeInBytes(); } return size; } /** Like getRecomputedSizeInBytes(), but, uses actual file * lengths rather than buffer allocations (which are * quantized up to nearest * RAMOutputStream.BUFFER_SIZE (now 1024) bytes. */ public final synchronized long getRecomputedActualSizeInBytes() throws IOException { if (!(delegate instanceof RAMDirectory)) return sizeInBytes(); long size = 0; for (final RAMFile file : ((RAMDirectory)delegate).fileMap.values()) size += file.length; return size; } @Override public synchronized void close() throws IOException { if (openFiles == null) { openFiles = new HashMap<String,Integer>(); } if (noDeleteOpenFile && openFiles.size() > 0) { // print the first one as its very verbose otherwise Exception cause = null; Iterator<Exception> stacktraces = files.values().iterator(); if (stacktraces.hasNext()) cause = stacktraces.next(); // RuntimeException instead of IOException because // super() does not throw IOException currently: throw new RuntimeException("MockDirectoryWrapper: cannot close: there are still open files: " + openFiles, cause); } open = false; delegate.close(); } boolean open = true; public synchronized boolean isOpen() { return open; } /** * Objects that represent fail-able conditions. Objects of a derived * class are created and registered with the mock directory. After * register, each object will be invoked once for each first write * of a file, giving the object a chance to throw an IOException. */ public static class Failure { /** * eval is called on the first write of every new file. */ public void eval(MockDirectoryWrapper dir) throws IOException { } /** * reset should set the state of the failure to its default * (freshly constructed) state. Reset is convenient for tests * that want to create one failure object and then reuse it in * multiple cases. This, combined with the fact that Failure * subclasses are often anonymous classes makes reset difficult to * do otherwise. * * A typical example of use is * Failure failure = new Failure() { ... }; * ... * mock.failOn(failure.reset()) */ public Failure reset() { return this; } protected boolean doFail; public void setDoFail() { doFail = true; } public void clearDoFail() { doFail = false; } } ArrayList<Failure> failures; /** * add a Failure object to the list of objects to be evaluated * at every potential failure point */ synchronized public void failOn(Failure fail) { if (failures == null) { failures = new ArrayList<Failure>(); } failures.add(fail); } /** * Iterate through the failures list, giving each object a * chance to throw an IOE */ synchronized void maybeThrowDeterministicException() throws IOException { if (failures != null) { for(int i = 0; i < failures.size(); i++) { failures.get(i).eval(this); } } } @Override public synchronized String[] listAll() throws IOException { return delegate.listAll(); } @Override public synchronized boolean fileExists(String name) throws IOException { return delegate.fileExists(name); } @Override public synchronized long fileModified(String name) throws IOException { return delegate.fileModified(name); } @Override public synchronized void touchFile(String name) throws IOException { delegate.touchFile(name); } @Override public synchronized long fileLength(String name) throws IOException { return delegate.fileLength(name); } @Override public synchronized Lock makeLock(String name) { return delegate.makeLock(name); } @Override public synchronized void clearLock(String name) throws IOException { delegate.clearLock(name); } @Override public synchronized void setLockFactory(LockFactory lockFactory) { delegate.setLockFactory(lockFactory); } @Override public synchronized LockFactory getLockFactory() { return delegate.getLockFactory(); } @Override public synchronized String getLockID() { return delegate.getLockID(); } @Override public synchronized void copy(Directory to, String src, String dest) throws IOException { delegate.copy(to, src, dest); } }