Merge idea133 into master

This CL merges the following CLs from aosp/idea133 into aosp/master.

dabd634b Snapshot e2750ea61032f2a041cb012bb7b90cffa0deba73 from idea/133.124 of git://git.jetbrains.org/idea/community.git
29ab773f Make default test target run Android specific tests
967117a7 Merge "Make default test target run Android specific tests" into idea133
1eb71f20 Bump memory settings for unit tests
b13ea0d4 Don't bind the delete key to restoring property in layout editor
a20ccfa9 Add Gradle import module
0980f4a2 Add gradle-import to build script
cee6f8ca Fix Gradle notification lookup on Windows.
8668e1be Snapshot 020d29497847701e84e383d965abf543b80758e2 from idea/133.370 of git://git.jetbrains.org/idea/community.git
18f77669 Merge remote-tracking branch 'aosp/snapshot-master' into merge
36ac8cba Update from Guava 13.0.1 to Guava 15
4d451f93 Update libraries to lombok 0.2.2
ee06b1d0 Remove lint-cli dependency from the idea project
ab73dade Updater: add unit tests.
dd558b6e Updater: on Windows, add "Retry" on file op failures.
f2f7178a Snapshot c11f3ac9bbde3f85d1f837ec3eb48a395ed7dd10 from idea/133.471 of git://git.jetbrains.org/idea/community.git
5e4c77db Merge remote-tracking branch 'aosp/snapshot-master' into merge
8d957349 Fix junit.jar path in updater project.
58c3e0ae Include cloud tools tests in default test group
63cd9779 Temporarily disable errors in project structure dialog
e2d6089d Snapshot b9931c55d2175b6552f90b2225eb09c13bd6dfed from idea/133.609 of git://git.jetbrains.org/idea/community.git
031a291e Merge remote-tracking branch 'aosp/snapshot-master' into merge
ea628d6e Remove versions from Info.plist
809cb3e7 Snapshot 9e6329d622cc9649c9c035f28faddc29564a5b7a from idea/133.696 of git://git.jetbrains.org/idea/community.git
d6cfe6ec Merge remote-tracking branch 'aosp/snapshot-master' into merge
38f8c6f0 Gracefully handle build.gradle files in modules without a configured JDK
70ae6f2a Snapshot dc1944e804515a346297e368c3b9c35a203c9912 from idea/133.818 of git://git.jetbrains.org/idea/community.git
ac91a6de Merge remote-tracking branch 'aosp/snapshot-master' into merge
2f51d957 Gradle: respect build classpath order and use both classes and sources jars if available
0ecdb509 Snapshot c50a8ad26a72432f26e39046d6a6f21fd7a190ee from idea/134.1160 of git://git.jetbrains.org/idea/community.git
e8c22ad7 Merge remote-tracking branch 'aosp/snapshot-master' into merge
72253f7d Turn off android framework detection
93f77ee6 Temporarily remove GCT tests
88f318c9 Snapshot 34f078c3452e79ba209d28a551962857e0970e5d from idea/134.1342 of git://git.jetbrains.org/idea/community.git
afb54e4b Merge remote-tracking branch 'aosp/snapshot-master' into merge
4dc795dc Fix updater UI tests.
57a49ed1 Studio patch: more logging.
aa614ee0 table greyer (ability to disable a table)
5c571417 Use Gradle model prebuilts v0.9.0 in Studio.
cb38c25d build script: Run jarjar on the updater
f273ca07 Add App Engine templates dir to build
7607404f Removed android-builder library from Studio (not needed.)
8f29b4eb Merge idea133 changes into master

Change-Id: I12231f26e886dbf5e2e5ac0b1c4bfe18f274d78f
diff --git a/updater/src/com/intellij/updater/BaseUpdateAction.java b/updater/src/com/intellij/updater/BaseUpdateAction.java
old mode 100644
new mode 100755
index 2ddfb68..67a16f8e
--- a/updater/src/com/intellij/updater/BaseUpdateAction.java
+++ b/updater/src/com/intellij/updater/BaseUpdateAction.java
@@ -35,7 +35,13 @@
 
   protected void replaceUpdated(File from, File dest) throws IOException {
     // on OS X code signing caches seem to be associated with specific file ids, so we need to remove the original file.
-    if (!dest.delete()) throw new IOException("Cannot delete file " + dest);
+    if (!dest.delete()) {
+      if (Utils.isWindows()) {
+        throw new RetryException("Cannot delete file " + dest);
+      } else {
+        throw new IOException("Cannot delete file " + dest);
+      }
+    }
     Utils.copy(from, dest);
   }
 
@@ -60,6 +66,7 @@
 
   protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, ZipOutputStream patchOutput)
     throws IOException {
+    Runner.logger.info("writing diff");
     ByteArrayOutputStream diffOutput = new ByteArrayOutputStream();
     byte[] newerFileBuffer = JBDiff.bsdiff(olderFileIn, newerFileIn, diffOutput);
     diffOutput.close();
diff --git a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java
index 677502e..c1114dd 100644
--- a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java
+++ b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java
@@ -10,6 +10,7 @@
 
   public void startProcess(String title) {
     System.out.println(title);
+    Runner.logger.info("title: " + title);
   }
 
   public void setProgress(int percentage) {
@@ -20,6 +21,7 @@
 
   public void setStatus(String status) {
     System.out.println(myStatus = status);
+    Runner.logger.info("status: " + status);
   }
 
   public void showError(Throwable e) {
diff --git a/updater/src/com/intellij/updater/CreateAction.java b/updater/src/com/intellij/updater/CreateAction.java
old mode 100644
new mode 100755
index aeeb5ed..5b0f8df
--- a/updater/src/com/intellij/updater/CreateAction.java
+++ b/updater/src/com/intellij/updater/CreateAction.java
@@ -18,6 +18,7 @@
   }
 
   protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+    Runner.logger.info("building PatchFile");
     patchOutput.putNextEntry(new ZipEntry(myPath));
 
     writeExecutableFlag(patchOutput, newerFile);
@@ -58,7 +59,15 @@
 
   private static void prepareToWriteFile(File file) throws IOException {
     if (file.exists()) {
-      Utils.delete(file);
+      try {
+        Utils.delete(file);
+      } catch (IOException e) {
+        if (Utils.isWindows() && file.exists()) {
+          throw new RetryException(e);
+        } else {
+          throw e;
+        }
+      }
       return;
     }
 
diff --git a/updater/src/com/intellij/updater/DeleteAction.java b/updater/src/com/intellij/updater/DeleteAction.java
old mode 100644
new mode 100755
index 3dd1b85..286e471
--- a/updater/src/com/intellij/updater/DeleteAction.java
+++ b/updater/src/com/intellij/updater/DeleteAction.java
@@ -43,13 +43,23 @@
 
   @Override
   protected void doApply(ZipFile patchFile, File toFile) throws IOException {
-    Utils.delete(toFile);
+    try {
+      Utils.delete(toFile);
+    } catch (IOException e) {
+      if (Utils.isWindows() && toFile.exists()) {
+        throw new RetryException(e);
+      } else {
+        throw e;
+      }
+    }
   }
 
+  @Override
   protected void doBackup(File toFile, File backupFile) throws IOException {
     Utils.copy(toFile, backupFile);
   }
 
+  @Override
   protected void doRevert(File toFile, File backupFile) throws IOException {
     Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions
     Utils.copy(backupFile, toFile);
diff --git a/updater/src/com/intellij/updater/Digester.java b/updater/src/com/intellij/updater/Digester.java
old mode 100644
new mode 100755
index 326f829..ba6a470
--- a/updater/src/com/intellij/updater/Digester.java
+++ b/updater/src/com/intellij/updater/Digester.java
@@ -28,6 +28,8 @@
         zipFile = new ZipFile(file);
       }
       catch (IOException e) {
+        // If this isn't a zip file, this isn't really an error, merely an info.
+        Runner.infoStackTrace("Can't open file as zip file: " + file.getPath() + "\n", e);
         return doDigestRegularFile(file);
       }
 
diff --git a/updater/src/com/intellij/updater/Patch.java b/updater/src/com/intellij/updater/Patch.java
index 71a9a52..9b66535 100644
--- a/updater/src/com/intellij/updater/Patch.java
+++ b/updater/src/com/intellij/updater/Patch.java
@@ -34,6 +34,7 @@
     throws IOException, OperationCancelledException {
     DiffCalculator.Result diff;
 
+    Runner.logger.info("Calculating difference...");
     ui.startProcess("Calculating difference...");
     ui.checkCancelled();
 
@@ -60,6 +61,7 @@
       }
     }
 
+    Runner.logger.info("Preparing actions...");
     ui.startProcess("Preparing actions...");
     ui.checkCancelled();
 
@@ -146,6 +148,7 @@
     final LinkedHashSet<String> files = Utils.collectRelativePaths(toDir);
     final List<ValidationResult> result = new ArrayList<ValidationResult>();
 
+    Runner.logger.info("Validating installation...");
     forEach(myActions, "Validating installation...", ui, true,
             new ActionsProcessor() {
               public void forEach(PatchAction each) throws IOException {
@@ -197,10 +200,12 @@
               });
     }
     catch (OperationCancelledException e) {
+      Runner.printStackTrace(e);
       shouldRevert = true;
       cancelled = true;
     }
     catch (Throwable e) {
+      Runner.printStackTrace(e);
       shouldRevert = true;
       ui.showError(e);
     }
diff --git a/updater/src/com/intellij/updater/PatchAction.java b/updater/src/com/intellij/updater/PatchAction.java
index 76259f0..42afb50 100644
--- a/updater/src/com/intellij/updater/PatchAction.java
+++ b/updater/src/com/intellij/updater/PatchAction.java
@@ -102,9 +102,11 @@
       return true;
     }
     catch (OverlappingFileLockException e) {
+      Runner.printStackTrace(e);
       return false;
     }
     catch (IOException e) {
+      Runner.printStackTrace(e);
       return false;
     }
   }
diff --git a/updater/src/com/intellij/updater/PatchFileCreator.java b/updater/src/com/intellij/updater/PatchFileCreator.java
index 4eab80c..e889237 100644
--- a/updater/src/com/intellij/updater/PatchFileCreator.java
+++ b/updater/src/com/intellij/updater/PatchFileCreator.java
@@ -20,7 +20,9 @@
                             List<String> criticalFiles,
                             List<String> optionalFiles,
                             UpdaterUI ui) throws IOException, OperationCancelledException {
+
     Patch patchInfo = new Patch(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui);
+    Runner.logger.info("Creating the patch file '" + patchFile + "'...");
     ui.startProcess("Creating the patch file '" + patchFile + "'...");
     ui.checkCancelled();
 
@@ -34,6 +36,8 @@
 
       List<PatchAction> actions = patchInfo.getActions();
       for (PatchAction each : actions) {
+
+        Runner.logger.info("Packing " + each.getPath());
         ui.setStatus("Packing " + each.getPath());
         ui.checkCancelled();
         each.buildPatchFile(olderDir, newerDir, out);
diff --git a/updater/src/com/intellij/updater/RetryException.java b/updater/src/com/intellij/updater/RetryException.java
new file mode 100755
index 0000000..e13c4eb
--- /dev/null
+++ b/updater/src/com/intellij/updater/RetryException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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 com.intellij.updater;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when an IOException arises when performing a patch
+ * action and it's likely that retrying will be successful.
+ */
+public class RetryException extends IOException {
+  public RetryException() {
+  }
+
+  public RetryException(String message) {
+    super(message);
+  }
+
+  public RetryException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public RetryException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/updater/src/com/intellij/updater/Runner.java b/updater/src/com/intellij/updater/Runner.java
old mode 100644
new mode 100755
index 02c4550..9a88ea4
--- a/updater/src/com/intellij/updater/Runner.java
+++ b/updater/src/com/intellij/updater/Runner.java
@@ -1,5 +1,10 @@
 package com.intellij.updater;
 
+import org.apache.log4j.FileAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+
 import javax.swing.*;
 import java.io.*;
 import java.net.URI;
@@ -11,49 +16,108 @@
 import java.util.zip.ZipInputStream;
 
 public class Runner {
+  public static Logger logger = null;
+
   private static final String PATCH_FILE_NAME = "patch-file.zip";
   private static final String PATCH_PROPERTIES_ENTRY = "patch.properties";
   private static final String OLD_BUILD_DESCRIPTION = "old.build.description";
   private static final String NEW_BUILD_DESCRIPTION = "new.build.description";
 
   public static void main(String[] args) throws Exception {
-    if (args.length != 2 && args.length < 6) {
-      printUsage();
-      return;
-    }
-
-    String command = args[0];
-
-    if ("create".equals(command)) {
-      if (args.length < 6) {
-        printUsage();
-        return;
-      }
+    if (args.length >= 7 && "create".equals(args[0])) {
       String oldVersionDesc = args[1];
       String newVersionDesc = args[2];
       String oldFolder = args[3];
       String newFolder = args[4];
       String patchFile = args[5];
+
+      String logFolder = args[6];
+      initLogger(logFolder);
+
       List<String> ignoredFiles = extractFiles(args, "ignored");
       List<String> criticalFiles = extractFiles(args, "critical");
       List<String> optionalFiles = extractFiles(args, "optional");
       create(oldVersionDesc, newVersionDesc, oldFolder, newFolder, patchFile, ignoredFiles, criticalFiles, optionalFiles);
     }
-    else if ("install".equals(command)) {
-      if (args.length != 2) {
-        printUsage();
-        return;
+    else if (args.length >= 2 && "install".equals(args[0])) {
+      // install [--exit0] <destination_folder> [log_directory]
+      int max = 3;
+      int nextArg = 1;
+
+      // Default install exit code is SwingUpdaterUI.RESULT_REQUIRES_RESTART (42) unless overridden to be 0.
+      // This is used by testUI/build.gradle as gradle expects a javaexec to exit with code 0.
+      boolean useExitCode0 = false;
+      if (args[nextArg].equals("--exit0")) {
+        useExitCode0 = true;
+        nextArg++;
+        max++;
       }
 
-      String destFolder = args[1];
-      install(destFolder);
+      String destFolder = args[nextArg++];
+
+      String logFolder = args.length >= max ? args[nextArg] : null;
+      initLogger(logFolder);
+      logger.info("destFolder: " + destFolder);
+
+      install(useExitCode0, destFolder);
     }
     else {
       printUsage();
-      return;
     }
   }
 
+  // checks that log directory 1)exists 2)has write perm. and 3)has 1MB+ free space
+  private static boolean isValidLogDir(String logFolder) {
+    File fileLogDir = new File(logFolder);
+    return fileLogDir.isDirectory() && fileLogDir.canWrite() && fileLogDir.getUsableSpace() >= 1000000;
+  }
+
+  private static String getLogDir(String logFolder) {
+    if (logFolder == null || !isValidLogDir(logFolder)) {
+      logFolder = System.getProperty("java.io.tmpdir");
+      if (!isValidLogDir(logFolder)) {
+        logFolder = System.getProperty("user.home");
+      }
+    }
+    return logFolder;
+  }
+
+  public static void initLogger(String logFolder) {
+    if (logger == null) {
+      logFolder = getLogDir(logFolder);
+      FileAppender update = new FileAppender();
+
+      update.setFile(new File(logFolder, "idea_updater.log").getAbsolutePath());
+      update.setLayout(new PatternLayout("%d{dd MMM yyyy HH:mm:ss} %-5p %C{1}.%M - %m%n"));
+      update.setThreshold(Level.ALL);
+      update.setAppend(true);
+      update.activateOptions();
+
+      FileAppender updateError = new FileAppender();
+      updateError.setFile(new File(logFolder, "idea_updater_error.log").getAbsolutePath());
+      updateError.setLayout(new PatternLayout("%d{dd MMM yyyy HH:mm:ss} %-5p %C{1}.%M - %m%n"));
+      updateError.setThreshold(Level.ERROR);
+      // The error(s) from an old run of the updater (if there were) could be found in idea_updater.log file
+      updateError.setAppend(false);
+      updateError.activateOptions();
+
+      logger = Logger.getLogger("com.intellij.updater");
+      logger.addAppender(updateError);
+      logger.addAppender(update);
+      logger.setLevel(Level.ALL);
+
+      logger.info("--- Updater started ---");
+    }
+  }
+
+  public static void infoStackTrace(String msg, Throwable e){
+    logger.info(msg, e);
+  }
+
+  public static void printStackTrace(Throwable e){
+    logger.error(e.getMessage(), e);
+  }
+
   public static List<String> extractFiles(String[] args, String paramName) {
     List<String> result = new ArrayList<String>();
     for (String param : args) {
@@ -68,10 +132,12 @@
     return result;
   }
 
+  @SuppressWarnings("UseOfSystemOutOrSystemErr")
   private static void printUsage() {
     System.err.println("Usage:\n" +
-                       "create <old_version_description> <new_version_description> <old_version_folder> <new_version_folder> <patch_file_name> [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" +
-                       "install <destination_folder>\n");
+                       "create <old_version_description> <new_version_description> <old_version_folder> <new_version_folder>" +
+                       " <patch_file_name> <log_directory> [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" +
+                       "install [--exit0] <destination_folder> [log_directory]\n");
   }
 
   private static void create(String oldBuildDesc,
@@ -82,9 +148,31 @@
                              List<String> ignoredFiles,
                              List<String> criticalFiles,
                              List<String> optionalFiles) throws IOException, OperationCancelledException {
-    UpdaterUI ui = new ConsoleUpdaterUI();
+    File tempPatchFile = Utils.createTempFile();
+    createImpl(oldBuildDesc,
+               newBuildDesc,
+               oldFolder,
+               newFolder,
+               patchFile,
+               tempPatchFile,
+               ignoredFiles,
+               criticalFiles,
+               optionalFiles,
+               new ConsoleUpdaterUI(), resolveJarFile());
+  }
+
+  static void createImpl(String oldBuildDesc,
+                         String newBuildDesc,
+                         String oldFolder,
+                         String newFolder,
+                         String outPatchJar,
+                         File   tempPatchFile,
+                         List<String> ignoredFiles,
+                         List<String> criticalFiles,
+                         List<String> optionalFiles,
+                         UpdaterUI ui,
+                         File resolvedJar) throws IOException, OperationCancelledException {
     try {
-      File tempPatchFile = Utils.createTempFile();
       PatchFileCreator.create(new File(oldFolder),
                               new File(newFolder),
                               tempPatchFile,
@@ -93,10 +181,13 @@
                               optionalFiles,
                               ui);
 
-      ui.startProcess("Packing jar file '" + patchFile + "'...");
-      ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(patchFile));
+      logger.info("Packing jar file: " + outPatchJar );
+      ui.startProcess("Packing jar file '" + outPatchJar + "'...");
+
+      FileOutputStream fileOut = new FileOutputStream(outPatchJar);
       try {
-        ZipInputStream in = new ZipInputStream(new FileInputStream(resolveJarFile()));
+        ZipOutputWrapper out = new ZipOutputWrapper(fileOut);
+        ZipInputStream in = new ZipInputStream(new FileInputStream(resolvedJar));
         try {
           ZipEntry e;
           while ((e = in.getNextEntry()) != null) {
@@ -110,8 +201,8 @@
         ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
         try {
           Properties props = new Properties();
-          props.put(OLD_BUILD_DESCRIPTION, oldBuildDesc);
-          props.put(NEW_BUILD_DESCRIPTION, newBuildDesc);
+          props.setProperty(OLD_BUILD_DESCRIPTION, oldBuildDesc);
+          props.setProperty(NEW_BUILD_DESCRIPTION, newBuildDesc);
           props.store(byteOut, "");
         }
         finally {
@@ -120,9 +211,10 @@
 
         out.zipBytes(PATCH_PROPERTIES_ENTRY, byteOut);
         out.zipFile(PATCH_FILE_NAME, tempPatchFile);
+        out.finish();
       }
       finally {
-        out.close();
+        fileOut.close();
       }
     }
     finally {
@@ -131,12 +223,13 @@
   }
 
   private static void cleanup(UpdaterUI ui) throws IOException {
+    logger.info("Cleaning up...");
     ui.startProcess("Cleaning up...");
     ui.setProgressIndeterminate();
     Utils.cleanup();
   }
 
-  private static void install(final String destFolder) throws Exception {
+  private static void install(final boolean useExitCode0, final String destFolder) throws Exception {
     InputStream in = Runner.class.getResourceAsStream("/" + PATCH_PROPERTIES_ENTRY);
     Properties props = new Properties();
     try {
@@ -153,25 +246,45 @@
           UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
         }
         catch (Exception ignore) {
+          printStackTrace(ignore);
         }
       }
     });
 
     new SwingUpdaterUI(props.getProperty(OLD_BUILD_DESCRIPTION),
                   props.getProperty(NEW_BUILD_DESCRIPTION),
+                  useExitCode0 ? 0 : SwingUpdaterUI.RESULT_REQUIRES_RESTART,
                   new SwingUpdaterUI.InstallOperation() {
+                    @Override
                     public boolean execute(UpdaterUI ui) throws OperationCancelledException {
+                      logger.info("installing patch to the " + destFolder);
                       return doInstall(ui, destFolder);
                     }
                   });
   }
 
+  interface IJarResolver {
+    File resolveJar() throws IOException;
+  }
+
   private static boolean doInstall(UpdaterUI ui, String destFolder) throws OperationCancelledException {
+    return doInstallImpl(ui, destFolder, new IJarResolver() {
+      @Override
+      public File resolveJar() throws IOException {
+        return resolveJarFile();
+      }
+    });
+  }
+
+  static boolean doInstallImpl(UpdaterUI ui,
+                               String destFolder,
+                               IJarResolver jarResolver) throws OperationCancelledException {
     try {
       try {
         File patchFile = Utils.createTempFile();
-        ZipFile jarFile = new ZipFile(resolveJarFile());
+        ZipFile jarFile = new ZipFile(jarResolver.resolveJar());
 
+        logger.info("Extracting patch file...");
         ui.startProcess("Extracting patch file...");
         ui.setProgressIndeterminate();
         try {
@@ -198,6 +311,7 @@
       }
       catch (IOException e) {
         ui.showError(e);
+        printStackTrace(e);
       }
     }
     finally {
@@ -206,6 +320,7 @@
       }
       catch (IOException e) {
         ui.showError(e);
+        printStackTrace(e);
       }
     }
 
@@ -229,6 +344,7 @@
       return new File(new URI(jarFileUrl));
     }
     catch (URISyntaxException e) {
+      printStackTrace(e);
       throw new IOException(e.getMessage());
     }
   }
diff --git a/updater/src/com/intellij/updater/SwingUpdaterUI.java b/updater/src/com/intellij/updater/SwingUpdaterUI.java
old mode 100644
new mode 100755
index 75af634..749dcc2
--- a/updater/src/com/intellij/updater/SwingUpdaterUI.java
+++ b/updater/src/com/intellij/updater/SwingUpdaterUI.java
@@ -10,6 +10,7 @@
 import java.awt.event.ActionListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.lang.reflect.InvocationTargetException;
@@ -19,7 +20,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class SwingUpdaterUI implements UpdaterUI {
-  private static final int RESULT_REQUIRES_RESTART = 42;
+  static final int RESULT_REQUIRES_RESTART = 42;
 
   private static final EmptyBorder FRAME_BORDER = new EmptyBorder(8, 8, 8, 8);
   private static final EmptyBorder LABEL_BORDER = new EmptyBorder(0, 0, 5, 0);
@@ -32,6 +33,7 @@
 
   private static final String PROCEED_BUTTON_TITLE = "Proceed";
 
+  private final int mySuccessExitCode;
   private final InstallOperation myOperation;
 
   private final JLabel myProcessTitle;
@@ -40,16 +42,30 @@
   private final JTextArea myConsole;
   private final JPanel myConsolePane;
 
+  private final JButton myRetryButton;
   private final JButton myCancelButton;
 
   private final ConcurrentLinkedQueue<UpdateRequest> myQueue = new ConcurrentLinkedQueue<UpdateRequest>();
   private final AtomicBoolean isCancelled = new AtomicBoolean(false);
   private final AtomicBoolean isRunning = new AtomicBoolean(false);
   private final AtomicBoolean hasError = new AtomicBoolean(false);
+  private final AtomicBoolean hasRetry = new AtomicBoolean(false);
   private final JFrame myFrame;
   private boolean myApplied;
 
-  public SwingUpdaterUI(String oldBuildDesc, String newBuildDesc, InstallOperation operation) {
+  /**
+   * Displays the updater UI and asynchronously runs the operation list.
+   *
+   * @param oldBuildDesc The old build description, for display purposes.
+   * @param newBuildDesc The new build description, for display purposes.
+   * @param successExitCode The desired exit code on success. Default is {@link #RESULT_REQUIRES_RESTART}.
+   * @param operation The install operations to perform.
+   */
+  public SwingUpdaterUI(String oldBuildDesc,
+                        String newBuildDesc,
+                        int successExitCode,
+                        InstallOperation operation) {
+    mySuccessExitCode = successExitCode;
     myOperation = operation;
 
     myProcessTitle = new JLabel(" ");
@@ -58,22 +74,35 @@
 
     myCancelButton = new JButton(CANCEL_BUTTON_TITLE);
 
+    myRetryButton = new JButton("Retry");
+    myRetryButton.setEnabled(false);
+    myRetryButton.setVisible(false);
+
     myConsole = new JTextArea();
     myConsole.setLineWrap(true);
     myConsole.setWrapStyleWord(true);
     myConsole.setCaretPosition(myConsole.getText().length());
     myConsole.setTabSize(1);
+    myConsole.setMargin(new Insets(2, 4, 2, 4));
     myConsolePane = new JPanel(new BorderLayout());
     myConsolePane.add(new JScrollPane(myConsole));
     myConsolePane.setBorder(BUTTONS_BORDER);
     myConsolePane.setVisible(false);
 
     myCancelButton.addActionListener(new ActionListener() {
+      @Override
       public void actionPerformed(ActionEvent e) {
         doCancel();
       }
     });
 
+    myRetryButton.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        doRetry();
+      }
+    });
+
     myFrame = new JFrame();
     myFrame.setTitle(TITLE);
 
@@ -103,6 +132,7 @@
     buttonsPanel.setBorder(BUTTONS_BORDER);
     buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS));
     buttonsPanel.add(Box.createHorizontalGlue());
+    buttonsPanel.add(myRetryButton);
     buttonsPanel.add(myCancelButton);
 
     myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "...");
@@ -128,12 +158,14 @@
 
   private void startRequestDispatching() {
     new Thread(new Runnable() {
+      @Override
       public void run() {
         while (true) {
           try {
             Thread.sleep(100);
           }
           catch (InterruptedException e) {
+            Runner.printStackTrace(e);
             return;
           }
 
@@ -144,6 +176,7 @@
           }
 
           SwingUtilities.invokeLater(new Runnable() {
+            @Override
             public void run() {
               for (UpdateRequest each : pendingRequests) {
                 each.perform();
@@ -170,22 +203,46 @@
     }
   }
 
+  private void doRetry() {
+    hasError.set(false);
+    hasRetry.set(false);
+    isCancelled.set(false);
+    myQueue.add(new UpdateRequest() {
+      @Override
+      public void perform() {
+        myConsole.setText("");
+        myConsolePane.setVisible(false);
+        myConsolePane.setPreferredSize(new Dimension(10, 200));
+        myRetryButton.setEnabled(false);
+        myCancelButton.setEnabled(true);
+      }
+    });
+    doPerform();
+  }
+
   private void doPerform() {
     isRunning.set(true);
 
     new Thread(new Runnable() {
+      @Override
       public void run() {
         try {
           myApplied = myOperation.execute(SwingUpdaterUI.this);
         }
         catch (OperationCancelledException ignore) {
+          Runner.printStackTrace(ignore);
         }
         catch(Throwable e) {
+          Runner.printStackTrace(e);
           showError(e);
         }
         finally {
           isRunning.set(false);
 
+          if (hasRetry.get()) {
+            myRetryButton.setVisible(true);
+            myRetryButton.setEnabled(true);
+          }
           if (hasError.get()) {
             startProcess("Failed to apply patch");
             setProgress(100);
@@ -200,15 +257,17 @@
   }
 
   private void exit() {
-    System.exit(myApplied ? RESULT_REQUIRES_RESTART : 0);
+    System.exit(myApplied ? mySuccessExitCode : 0);
   }
 
+  @Override
   public Map<String, ValidationResult.Option> askUser(final List<ValidationResult> validationResults) throws OperationCancelledException {
     if (validationResults.isEmpty()) return Collections.emptyMap();
 
     final Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>();
     try {
       SwingUtilities.invokeAndWait(new Runnable() {
+        @Override
         public void run() {
           final JDialog dialog = new JDialog(myFrame, TITLE, true);
           dialog.setLayout(new BorderLayout());
@@ -220,6 +279,7 @@
           buttonsPanel.add(Box.createHorizontalGlue());
           JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE);
           proceedButton.addActionListener(new ActionListener() {
+            @Override
             public void actionPerformed(ActionEvent e) {
               dialog.setVisible(false);
             }
@@ -227,6 +287,7 @@
 
           JButton cancelButton = new JButton(CANCEL_BUTTON_TITLE);
           cancelButton.addActionListener(new ActionListener() {
+            @Override
             public void actionPerformed(ActionEvent e) {
               isCancelled.set(true);
               myCancelButton.setEnabled(false);
@@ -283,8 +344,10 @@
     return result;
   }
 
+  @Override
   public void startProcess(final String title) {
     myQueue.add(new UpdateRequest() {
+      @Override
       public void perform() {
         myProcessStatus.setText(title);
         myProcessProgress.setIndeterminate(false);
@@ -293,8 +356,10 @@
     });
   }
 
+  @Override
   public void setProgress(final int percentage) {
     myQueue.add(new UpdateRequest() {
+      @Override
       public void perform() {
         myProcessProgress.setIndeterminate(false);
         myProcessProgress.setValue(percentage);
@@ -302,34 +367,63 @@
     });
   }
 
+  @Override
   public void setProgressIndeterminate() {
     myQueue.add(new UpdateRequest() {
+      @Override
       public void perform() {
         myProcessProgress.setIndeterminate(true);
       }
     });
   }
 
+  @Override
   public void setStatus(final String status) {
   }
 
+  @Override
   public void showError(final Throwable e) {
+    hasError.set(true);
+    StringWriter w = new StringWriter();
+
+    if (e instanceof RetryException) {
+      hasRetry.set(true);
+
+      w.write("+----------------\n");
+      w.write("| A file operation failed.\n");
+      w.write("| This might be due to a file being locked by another\n");
+      w.write("| application. Please try closing any application\n");
+      w.write("| that uses the files being updated then press 'Retry'.\n");
+      w.write("+----------------\n");
+      w.write("\n\n");
+    }
+
+    e.printStackTrace(new PrintWriter(w));
+
+    final String content = w.getBuffer().toString();
+
     myQueue.add(new UpdateRequest() {
+      @Override
       public void perform() {
         StringWriter w = new StringWriter();
-        e.printStackTrace(new PrintWriter(w));
-        w.append("\n");
-        myConsole.append(w.getBuffer().toString());
         if (!myConsolePane.isVisible()) {
+          w.write("Temp. directory: ");
+          w.write(System.getProperty("java.io.tmpdir"));
+          w.write("\n\n");
+        }
+        myConsole.append(w.getBuffer().toString());
+        myConsole.append(content);
+        if (!myConsolePane.isVisible()) {
+          myConsole.setCaretPosition(0);
           myConsolePane.setVisible(true);
           myConsolePane.setPreferredSize(new Dimension(10, 200));
           myFrame.pack();
         }
-        hasError.set(true);
       }
     });
   }
 
+  @Override
   public void checkCancelled() throws OperationCancelledException {
     if (isCancelled.get()) throw new OperationCancelledException();
   }
@@ -343,7 +437,8 @@
   }
 
   public static void main(String[] args) {
-    new SwingUpdaterUI("xxx", "yyy", new InstallOperation() {
+    new SwingUpdaterUI("xxx", "yyy", RESULT_REQUIRES_RESTART, new InstallOperation() {
+      @Override
       public boolean execute(UpdaterUI ui) throws OperationCancelledException {
         ui.startProcess("Process1");
         ui.checkCancelled();
@@ -427,6 +522,7 @@
       }
     }
 
+    @Override
     public int getColumnCount() {
       return COLUMNS.length;
     }
@@ -453,6 +549,7 @@
       return super.getColumnClass(columnIndex);
     }
 
+    @Override
     public int getRowCount() {
       return myItems.size();
     }
@@ -469,6 +566,7 @@
       }
     }
 
+    @Override
     public Object getValueAt(int rowIndex, int columnIndex) {
       Item item = myItems.get(rowIndex);
       switch (columnIndex) {
diff --git a/updater/src/com/intellij/updater/UpdateZipAction.java b/updater/src/com/intellij/updater/UpdateZipAction.java
index 553e7d8..7741b81 100644
--- a/updater/src/com/intellij/updater/UpdateZipAction.java
+++ b/updater/src/com/intellij/updater/UpdateZipAction.java
@@ -103,6 +103,8 @@
       new ZipFile(newerFile).close();
     }
     catch (IOException e) {
+      Runner.logger.error("Corrupted target file: " + newerFile);
+      Runner.printStackTrace(e);
       throw new IOException("Corrupted target file: " + newerFile, e);
     }
 
@@ -115,6 +117,8 @@
       olderZip = new ZipFile(olderFile);
     }
     catch (IOException e) {
+      Runner.logger.error("Corrupted source file: " + olderFile);
+      Runner.printStackTrace(e);
       throw new IOException("Corrupted source file: " + olderFile, e);
     }
 
@@ -137,6 +141,8 @@
             patchOutput.closeEntry();
           }
           catch (IOException e) {
+            Runner.logger.error("Error building patch for .zip entry " + name);
+            Runner.printStackTrace(e);
             throw new IOException("Error building patch for .zip entry " + name, e);
           }
         }
@@ -149,11 +155,11 @@
 
   protected void doApply(final ZipFile patchFile, File toFile) throws IOException {
     File temp = Utils.createTempFile();
-    @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
-    final ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(temp));
-    out.setCompressionLevel(0);
-
+    FileOutputStream fileOut = new FileOutputStream(temp);
     try {
+      final ZipOutputWrapper out = new ZipOutputWrapper(fileOut);
+      out.setCompressionLevel(0);
+
       processZipFile(toFile, new Processor() {
         public void process(ZipEntry entry, InputStream in) throws IOException {
           String path = entry.getName();
@@ -183,9 +189,11 @@
           in.close();
         }
       }
+
+      out.finish();
     }
     finally {
-      out.close();
+      fileOut.close();
     }
 
     replaceUpdated(temp, toFile);
diff --git a/updater/src/com/intellij/updater/Utils.java b/updater/src/com/intellij/updater/Utils.java
old mode 100644
new mode 100755
index 427788e..1e6407d
--- a/updater/src/com/intellij/updater/Utils.java
+++ b/updater/src/com/intellij/updater/Utils.java
@@ -10,61 +10,97 @@
   private static final byte[] BUFFER = new byte[64 * 1024];
   private static File myTempDir;
 
+  public static boolean isWindows() {
+    return System.getProperty("os.name").startsWith("Windows");
+  }
+
   public static boolean isZipFile(String fileName) {
     return fileName.endsWith(".zip") || fileName.endsWith(".jar");
   }
 
+  /**
+   * Creates a new temp file. <br/>
+   * All the temp files created here are located in a unique root temp directory
+   * that is automatically deleted by {@link #cleanup()}.
+   */
   @SuppressWarnings({"SSBasedInspection"})
   public static File createTempFile() throws IOException {
     if (myTempDir == null) {
-      myTempDir = File.createTempFile("idea.updater", "tmp");
+      myTempDir = File.createTempFile("idea.updater.", ".tmp");
       delete(myTempDir);
       myTempDir.mkdirs();
+      Runner.logger.info("created temp file: " + myTempDir.getPath());
     }
 
-    return File.createTempFile("temp", "tmp", myTempDir);
+    return File.createTempFile("temp.", ".tmp", myTempDir);
   }
 
+
+  /**
+   * Creates a new temp directory. <br/>
+   * All the temp directories created here are located in a unique root temp directory
+   * that is automatically deleted by {@link #cleanup()}.
+   */
   public static File createTempDir() throws IOException {
     File result = createTempFile();
     delete(result);
+    Runner.logger.info("deleted tmp dir: " + result.getPath());
     result.mkdirs();
+    Runner.logger.info("created tmp dir: " + result.getPath());
     return result;
   }
 
   public static void cleanup() throws IOException {
     if (myTempDir == null) return;
     delete(myTempDir);
+    Runner.logger.info("deleted file " + myTempDir.getPath());
     myTempDir = null;
   }
 
+  /**
+   * Deletes a file or directory with a default timeout of 100 milliseconds.
+   * Directories are deleted recursively. The timeout occurs on each file.
+   * If one of the files fails to be deleted, the recursive directory deletion
+   * is aborted and not retried.
+   *
+   * @param file The file or directory to delete.
+   * @throws IOException
+   */
   public static void delete(File file) throws IOException {
     if (file.isDirectory()) {
       File[] files = file.listFiles();
       if (files != null) {
         for (File each : files) {
           delete(each);
+          Runner.logger.info("deleted file " + each.getPath());
         }
       }
     }
+
     for (int i = 0; i < 10; i++) {
-      if (file.delete() || !file.exists()) return;
+      if (file.delete() || !file.exists()) {
+        return;
+      }
       try {
         Thread.sleep(10);
-      }
-      catch (InterruptedException ignore) {
+      } catch (InterruptedException ignore) {
+        Runner.printStackTrace(ignore);
       }
     }
-    if (file.exists()) throw new IOException("Cannot delete file " + file);
+    if (file.exists()) {
+      throw new IOException("Cannot delete file " + file);
+    }
   }
 
   public static void setExecutable(File file, boolean executable) throws IOException {
     if (executable && !file.setExecutable(true)) {
+      Runner.logger.error("Can't set executable permissions for file");
       throw new IOException("Cannot set executable permissions for: " + file);
     }
   }
 
   public static void copy(File from, File to) throws IOException {
+    Runner.logger.info("from " + from.getPath() + " to " + to.getPath());
     if (from.isDirectory()) {
       File[] files = from.listFiles();
       if (files == null) throw new IOException("Cannot get directory's content: " + from);
@@ -141,6 +177,7 @@
   public static InputStream getEntryInputStream(ZipFile zipFile, String entryPath) throws IOException {
     InputStream result = findEntryInputStream(zipFile, entryPath);
     if (result == null) throw new IOException("Entry " + entryPath + " not found");
+    Runner.logger.info("entryPath: " + entryPath);
     return result;
   }
 
diff --git a/updater/src/com/intellij/updater/ZipOutputWrapper.java b/updater/src/com/intellij/updater/ZipOutputWrapper.java
index 1ccbc45..ec47546 100644
--- a/updater/src/com/intellij/updater/ZipOutputWrapper.java
+++ b/updater/src/com/intellij/updater/ZipOutputWrapper.java
@@ -74,14 +74,10 @@
     }
 
     myOut.putNextEntry(entry);
-    try {
-      byteOut.writeTo(myOut);
-    }
-    finally {
-      myOut.closeEntry();
-    }
+    byteOut.writeTo(myOut);
+    myOut.closeEntry();
   }
-  
+
   public void zipFile(String entryPath, File file) throws IOException {
     if (file.isDirectory()) {
       addDirs(entryPath, true);
@@ -124,20 +120,17 @@
     myDirs.addAll(temp);
   }
 
-  public void close() throws IOException {
-    try {
-      for (String each : myDirs) {
-        if (!each.endsWith("/")) each += "/";
-        ZipEntry e = new ZipEntry(each);
-        e.setMethod(ZipEntry.STORED);
-        e.setSize(0);
-        e.setCrc(0);
-        myOut.putNextEntry(e);
-        myOut.closeEntry();
-      }
+  public void finish() throws IOException {
+    for (String each : myDirs) {
+      if (!each.endsWith("/")) each += "/";
+      ZipEntry e = new ZipEntry(each);
+      e.setMethod(ZipEntry.STORED);
+      e.setSize(0);
+      e.setCrc(0);
+      myOut.putNextEntry(e);
+      myOut.closeEntry();
     }
-    finally {
-      myOut.close();
-    }
+
+    myOut.close();
   }
 }
diff --git a/updater/testData/Readme.txt b/updater/testData/Readme.txt
new file mode 100644
index 0000000..c509af0
--- /dev/null
+++ b/updater/testData/Readme.txt
@@ -0,0 +1,166 @@
+IntelliJ IDEA 8.0M1 - README
+
+Thank you for downloading IntelliJ IDEA!
+
+IntelliJ IDEA is a multi-platform Java IDE, which includes intelligent editor, rich-featured GUI designer,
+visual debugger, javac/jikes/rmic compiler integration, refactoring, enhanced project navigation,
+intelligent code inspection and analysis features, J2EE and JDK 1.5 support.
+
+
+Contents:
+=========
+  BUILD.TXT           File containing the current build number
+  KnownIssues.TXT     Known issues and workarounds
+  README.TXT          This file
+  bin/                Startup scripts for launching IntelliJ IDEA
+  help/               Online help files
+  jre/                Bundled JRE
+  lib/                Library files
+  license/            License files for IntelliJ IDEA and bundled software
+  plugins/            Standard plugins
+  redist/             Contains libraries that need to be redistributed with your application if certain IDEA features are used:
+                      - forms_rt.jar should be distributed with your applications that use GUI forms with
+                        the "GridLayoutManager (IntelliJ)" layout manager
+                      - javac2.jar is an Ant task for building applications that use IntelliJ IDEA's UI Designer
+                        bytecode generation or @NotNull assertions generation
+                      - annotations.jar contains JDK 1.5 annotation classes for 'Constant Conditions & Exceptions' inspection tool.
+
+  Install-Linux-tar.txt     Installation instructions for Linux (included in Linux installation package only)
+  Install-Windows-zip.txt   Installation instructions for Windows zip-file  (included in Windows zip installation package only)
+
+  USER_HOME/.IntelliJIdea80/
+
+      config/         Configuration files (See INSTALLATION_HOME/bin/idea.properties to tweak location of the configs)
+        codestyles/       User's code style settings
+        colors/           User's colors and fonts settings
+        fileTemplates/    Custom file templates
+        filetypes/        Custom file types
+        ideTalk/          ideTalk settings
+        inspection/       Executable file and auxiliary data for running offline code inspections
+        keymaps/          Contains files with custom keymaps
+        migration/        API migration map
+        options/          IDE options configuration files
+        plugins/          Directory for custom plugins (it appears after the 1st plugin is installed)
+        shelf/            Shelved changes (in standard .patch file format)
+        templates/        Live templates, both built-in and custom
+        tools/            External tools
+        idea70.key        File containing your license key (editing not recommended)
+
+      system/         Various IDEA internal caches including Local History data storage.
+                      (See INSTALLATION_HOME/bin/idea.properties to tweak location of the caches).
+                      Also log directory with IDEA log files is located there.
+
+
+Installing IntelliJ IDEA
+========================
+  For Linux and other Generic Unix users:
+  Please read the contents of the Install-Linux-tar.txt file.
+
+  Installing on Mac OS:
+  IntelliJ IDEA is distributed as a .dmg file. You only need to drag it to
+  the destination folder, from which you can start the application.
+
+Uninstalling IntelliJ IDEA
+==========================
+  If you installed IntelliJ IDEA with the help of Installation Wizard, then
+  just run the INSTALLATION_HOME/bin/Uninstall.exe file
+
+  To uninstall IntelliJ IDEA after manual installation, simply delete the
+  contents of the IntelliJ IDEA home installation directory.
+
+
+Licensing & pricing
+==========================
+  Licensing and pricing information can be found at http://www.jetbrains.com/idea/buy/index.html.
+
+
+IntelliJ IDEA Overview
+==========================
+  For general info and facts on IntelliJ IDEA, you can refer to IntelliJ IDEA Info Kit at
+  http://www.jetbrains.com/idea/documentation/product_info_kit.html.
+
+
+IDEA Development Package
+==========================
+  Contains:
+  - Source code for the OpenAPI classes;
+  - Documentation (JavaDocs for the OpenAPI and a number of additional components);
+  - Simple example plugins demonstrating usage of the OpenAPI;
+  - Source code for plugins shipped with IDEA:
+      * Plugin Development Kit
+      * IDEtalk plugin
+      * Images support plugin
+      * Inspection Gadgets
+      * Intention PowerPack
+      * J2ME development support plugin
+      * JavaScript support plugin
+      * JavaScript inspections plugin
+      * JavaScript Intention PowerPack
+      * GWTStudio plugin
+      * KlassMaster stacktrace unscramble plugin
+      * StrutsAssistant plugin
+      * StarTeam, Perforce, Subversion, Visual SourceSafe integration
+      * Tomcat, Weblogic, WebSphere, Geronimo, JBoss, GlassFish, JSR45 integration
+
+  Download page: http://www.jetbrains.com/idea/download/index.html
+
+  Source code of additional open source plugins shipped with IntelliJ IDEA is available in the Subversion
+  repository at:
+  http://svn.jetbrains.org/idea/Trunk/bundled/
+
+
+Using Plugins
+=============
+  IDEA now smartly integrates with the Community web-site which holds a repository of the third-party plugins to it.
+  This integration, supplied with a convenient UI, helps you incorporate any available plugins without switching from
+  IDEA settings dialog (Settings | Plugins).
+
+  You can browse the plugins Web site, rate and comment on plugins at:
+  http://plugins.intellij.net/
+
+
+Home Page:
+==========
+  http://www.jetbrains.com
+
+
+IntelliJ Technology Network
+===========================
+  http://www.intellij.net
+  Early Access to new products, internal builds and patches, Bug/Features database, Forums/newsgroups
+
+
+IntelliJ Community Site
+=======================
+  http://www.intellij.org
+  The community-driven Wiki site dedicated to IntelliJ products and technologies.
+
+
+Support
+=======
+  For technical support and assistance, you may find necessary information at the Support page
+  (http://www.jetbrains.com/support/index.html) or contact us at [email protected].
+
+
+Bug Reporting:
+==============
+  Send emails to [email protected]
+
+
+Contacting us:
+==============
+  [email protected]       - Sales inquiries, billing, order processing questions
+  [email protected]     - Technical support (all products)
+  [email protected]    - Sales inquiries in the United States
+  [email protected]  - Technical support for US customers
+  [email protected] - Feature suggestions
+  [email protected]        - Product inquiries
+
+
+=============
+You are encouraged to visit our IntelliJ IDEA web site at http://www.jetbrains.com/idea/
+or to contact us via e-mail at [email protected] if you have any comments about
+this release. In particular, we are very interested in any ease-of-use, user
+interface suggestions that you may have. We will be posting regular updates
+of our progress to our online forums and newsgroups.
+=============
diff --git a/updater/testData/bin/focuskiller.dll b/updater/testData/bin/focuskiller.dll
new file mode 100644
index 0000000..c1f8e04
--- /dev/null
+++ b/updater/testData/bin/focuskiller.dll
Binary files differ
diff --git a/updater/testData/bin/idea.bat b/updater/testData/bin/idea.bat
new file mode 100644
index 0000000..79a087f
--- /dev/null
+++ b/updater/testData/bin/idea.bat
@@ -0,0 +1,66 @@
+@echo off
+
+::----------------------------------------------------------------------
+:: IntelliJ IDEA Startup Script
+::----------------------------------------------------------------------
+
+set JDK_HOME=c:/tools/jdk
+
+:: ---------------------------------------------------------------------
+:: Before you run IntelliJ IDEA specify the location of the
+:: JDK 1.5 installation directory which will be used for running IDEA
+:: ---------------------------------------------------------------------
+IF "%IDEA_JDK%" == "" SET IDEA_JDK=%JDK_HOME%
+IF "%IDEA_JDK%" == "" goto error
+
+:: ---------------------------------------------------------------------
+:: Before you run IntelliJ IDEA specify the location of the
+:: directory where IntelliJ IDEA is installed
+:: In most cases you do not need to change the settings below.
+:: ---------------------------------------------------------------------
+SET IDEA_HOME=..
+
+SET JAVA_EXE=%IDEA_JDK%\jre\bin\java.exe
+IF NOT EXIST "%JAVA_EXE%" goto error
+
+IF "%IDEA_MAIN_CLASS_NAME%" == "" SET IDEA_MAIN_CLASS_NAME=com.intellij.idea.Main
+
+IF NOT "%IDEA_PROPERTIES%" == "" set IDEA_PROPERTIES_PROPERTY=-Didea.properties.file=%IDEA_PROPERTIES%
+
+:: ---------------------------------------------------------------------
+:: You may specify your own JVM arguments in idea.exe.vmoptions file. Put one option per line there.
+:: ---------------------------------------------------------------------
+SET ACC=
+FOR /F "delims=" %%i in (%IDEA_HOME%\bin\idea.exe.vmoptions) DO call %IDEA_HOME%\bin\append.bat "%%i"
+
+set REQUIRED_IDEA_JVM_ARGS=-Xbootclasspath/p:%IDEA_HOME%/lib/boot.jar %IDEA_PROPERTIES_PROPERTY% %REQUIRED_IDEA_JVM_ARGS%
+SET JVM_ARGS=%ACC% %REQUIRED_IDEA_JVM_ARGS%
+
+SET OLD_PATH=%PATH%
+SET PATH=%IDEA_HOME%\bin;%PATH%
+
+SET CLASS_PATH=%IDEA_HOME%\lib\bootstrap.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_HOME%\lib\util.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_HOME%\lib\jdom.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_HOME%\lib\log4j.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_HOME%\lib\extensions.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_HOME%\lib\trove4j.jar
+SET CLASS_PATH=%CLASS_PATH%;%IDEA_JDK%\lib\tools.jar
+
+:: ---------------------------------------------------------------------
+:: You may specify additional class paths in IDEA_CLASS_PATH variable.
+:: It is a good idea to specify paths to your plugins in this variable.
+:: ---------------------------------------------------------------------
+IF NOT "%IDEA_CLASS_PATH%" == "" SET CLASS_PATH=%CLASS_PATH%;%IDEA_CLASS_PATH%
+
+"%JAVA_EXE%" %JVM_ARGS% -cp "%CLASS_PATH%" %IDEA_MAIN_CLASS_NAME% %*
+
+SET PATH=%OLD_PATH%
+goto end
+:error
+echo ---------------------------------------------------------------------
+echo ERROR: cannot start IntelliJ IDEA.
+echo No JDK found to run IDEA. Please validate either IDEA_JDK or JDK_HOME points to valid JDK installation
+echo ---------------------------------------------------------------------
+pause
+:end
diff --git a/updater/testData/bin/idea.properties b/updater/testData/bin/idea.properties
new file mode 100644
index 0000000..37bc736
--- /dev/null
+++ b/updater/testData/bin/idea.properties
@@ -0,0 +1,105 @@
+# Set up IDEA_PROPERTIES environment variable to specify custom location of this properties file like
+# SET IDEA_PROPERTIES=c:\ideaconfig\idea.properties
+# before launching idea.
+# If not specified it is searched according following sequence (first successful is used).
+# 1. ${user.home}
+# 2. ${idea.home}/bin
+
+# Use ${idea.home} macro to specify location relative to IDEA installation home
+# Also use ${xxx} where xxx is any java property (including defined in previous lines of this file) to refer to its value
+# Note for Windows users: please make sure you're using forward slashes. I.e. c:/idea/system
+
+#---------------------------------------------------------------------
+# Uncomment this option if you want to customize path to IDEA config folder. Make sure you're using forward slashes
+#---------------------------------------------------------------------
+idea.config.path=${idea.home}/config
+
+#---------------------------------------------------------------------
+# Uncomment this option if you want to customize path to IDEA system folder. Make sure you're using forward slashes
+#---------------------------------------------------------------------
+idea.system.path=${idea.home}/system
+
+#---------------------------------------------------------------------
+# Uncomment this option if you want to customize path to user installed plugins folder. Make sure you're using forward slashes
+#---------------------------------------------------------------------
+idea.plugins.path=${idea.home}/config/plugins
+
+#---------------------------------------------------------------------
+# Maximum file size (kilobytes) IDEA should provide intellisense for.
+# The larger file is the slower its editor works and higher overall system memory requirements are
+# if intellisense is enabled. Remove this property or set to very large number if you need
+# intellisense for any files available regardless their size.
+# Please note this option doesn't operate with Java files. Regardless of the option value intellisense will anyway stay there.
+#---------------------------------------------------------------------
+idea.max.intellisense.filesize=2048
+
+#---------------------------------------------------------------------
+# There are two possible values of idea.popup.weight property: "heavy" and "medium".
+# If you have WM configured as "Focus follows mouse with Auto Raise" then you have to
+# set this property to "medium". It prevents problems with popup menus on some
+# configurations.
+# ---------------------------------------------------------------------
+idea.popup.weight=heavy
+
+#----------------------------------------------------------------------
+# Disabling this property may lead to visual glitches like blinking and fail to repaint
+# on certain display adapter cards.
+#----------------------------------------------------------------------
+sun.java2d.noddraw=true
+
+#----------------------------------------------------------------------
+# Removing this property may lead to editor performance degradation under Windows.
+#----------------------------------------------------------------------
+sun.java2d.d3d=false
+
+#-----------------------------------------------------------------------
+# IDEA copies library jars to prevent their locking. If copying is not desirable, specify "true"
+#-----------------------------------------------------------------------
+idea.jars.nocopy=false
+
+#----------------------------------------------------------------------
+# Configure if a special launcher should be used when running processes from within IDEA.
+# Using Launcher enables "soft exit" and "thread dump" features
+#----------------------------------------------------------------------
+idea.no.launcher=false
+
+#-----------------------------------------------------------------------
+# The VM option value to be used start the JVM in debug mode.
+# Some environments define it in a different way (-XXdebug in Oracle VM)
+#-----------------------------------------------------------------------
+idea.xdebug.key=-Xdebug
+
+#-----------------------------------------------------------------------
+# Switch into JMX 1.0 compatible mode
+# Uncomment this option to be able to run IDEA using J2SDK 1.5 while working
+# with application servers (like WebLogic) running 1.4
+#-----------------------------------------------------------------------
+#jmx.serial.form=1.0
+
+#-----------------------------------------------------------------------
+# Uncomment this option if you don't like to receive notifications about
+# fatal errors that happen to IDEA or plugins installed.
+#-----------------------------------------------------------------------
+idea.fatal.error.notification=disabled
+
+# Workaround for slow scrolling in JDK6
+swing.bufferPerWindow=false
+
+#-----------------------------------------------------------------------
+# Uncomment this property to prevent IDEA from throwing ProcessCanceledException when user activity
+# detected. This option is only useful for plugin developers, while debugging PSI related activities
+# performed in background error analysis thread.
+# DO NOT UNCOMMENT THIS UNLESS YOU'RE DEBUGGING IDEA ITSELF. Significant slowdowns and lockups will happen otherwise.
+#-----------------------------------------------------------------------
+#idea.ProcessCanceledException=disabled
+
+#----------------------------------------------------------------------
+# Removing this property may lead to editor performance degradation under X-Windows.
+#----------------------------------------------------------------------
+sun.java2d.pmoffscreen=false
+
+#---------------------------------------------------------------------
+# Maximum size (kilobytes) IDEA will load for showing past file contents -
+# in Show Diff or when calculating Digest Diff
+#---------------------------------------------------------------------
+#idea.max.vcs.loaded.size.kb=20480
\ No newline at end of file
diff --git a/updater/testData/lib/annotations.jar b/updater/testData/lib/annotations.jar
new file mode 100644
index 0000000..53f730f
--- /dev/null
+++ b/updater/testData/lib/annotations.jar
Binary files differ
diff --git a/updater/testData/lib/annotations_changed.jar b/updater/testData/lib/annotations_changed.jar
new file mode 100644
index 0000000..43af95c
--- /dev/null
+++ b/updater/testData/lib/annotations_changed.jar
Binary files differ
diff --git a/updater/testData/lib/boot.jar b/updater/testData/lib/boot.jar
new file mode 100644
index 0000000..98b408f
--- /dev/null
+++ b/updater/testData/lib/boot.jar
Binary files differ
diff --git a/updater/testData/lib/boot2.jar b/updater/testData/lib/boot2.jar
new file mode 100644
index 0000000..5c9c316
--- /dev/null
+++ b/updater/testData/lib/boot2.jar
Binary files differ
diff --git a/updater/testData/lib/boot2_changed_with_unchanged_content.jar b/updater/testData/lib/boot2_changed_with_unchanged_content.jar
new file mode 100644
index 0000000..c8eb3d9
--- /dev/null
+++ b/updater/testData/lib/boot2_changed_with_unchanged_content.jar
Binary files differ
diff --git a/updater/testData/lib/boot_with_directory_becomes_file.jar b/updater/testData/lib/boot_with_directory_becomes_file.jar
new file mode 100644
index 0000000..4c16379
--- /dev/null
+++ b/updater/testData/lib/boot_with_directory_becomes_file.jar
Binary files differ
diff --git a/updater/testData/lib/bootstrap.jar b/updater/testData/lib/bootstrap.jar
new file mode 100644
index 0000000..3a32734
--- /dev/null
+++ b/updater/testData/lib/bootstrap.jar
Binary files differ
diff --git a/updater/testData/lib/bootstrap_deleted.jar b/updater/testData/lib/bootstrap_deleted.jar
new file mode 100644
index 0000000..b0aed41
--- /dev/null
+++ b/updater/testData/lib/bootstrap_deleted.jar
Binary files differ
diff --git a/updater/testSrc/com/intellij/updater/DigesterTest.java b/updater/testSrc/com/intellij/updater/DigesterTest.java
new file mode 100644
index 0000000..a7ec965
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/DigesterTest.java
@@ -0,0 +1,21 @@
+package com.intellij.updater;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+public class DigesterTest extends UpdaterTestCase {
+  @Test
+  public void testBasics() throws Exception {
+    Map<String, Long> checkSums = Digester.digestFiles(getDataDir(), Collections.<String>emptyList(), TEST_UI);
+    assertEquals(12, checkSums.size());
+
+    assertEquals(CHECKSUMS.README_TXT, (long)checkSums.get("Readme.txt"));
+    assertEquals(CHECKSUMS.FOCUSKILLER_DLL, (long)checkSums.get("bin/focuskiller.dll"));
+    assertEquals(CHECKSUMS.BOOTSTRAP_JAR, (long)checkSums.get("lib/bootstrap.jar"));
+    Runner.initLogger(System.getProperty("java.io.tmpdir"));
+  }
+}
\ No newline at end of file
diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java
new file mode 100644
index 0000000..4e87db2
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java
@@ -0,0 +1,333 @@
+package com.intellij.updater;
+
+import com.intellij.openapi.util.io.FileUtil;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.Assert.*;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class PatchFileCreatorTest extends PatchTestCase {
+  private File myFile;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    myFile = getTempFile("patch.zip");
+  }
+
+  @Test
+  public void testCreatingAndApplying() throws Exception {
+    createPatch();
+
+    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void failOnEmptySourceJar() throws Exception {
+    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
+    if (sourceJar.exists()) sourceJar.delete();
+    assertTrue(sourceJar.createNewFile());
+
+    try {
+      final File targetJar = new File(myNewerDir, "lib/empty.jar");
+      FileUtil.copy(new File(myNewerDir, "lib/annotations.jar"), targetJar);
+
+      try {
+        createPatch();
+        fail("Should have failed to create a patch from empty .jar");
+      }
+      catch (IOException e) {
+        final String reason = e.getMessage();
+        assertEquals("Corrupted source file: " + sourceJar, reason);
+      }
+      finally {
+        targetJar.delete();
+      }
+    }
+    finally {
+      sourceJar.delete();
+    }
+  }
+
+  @Test
+  public void failOnEmptyTargetJar() throws Exception {
+    final File sourceJar = new File(myOlderDir, "lib/empty.jar");
+    FileUtil.copy(new File(myOlderDir, "lib/annotations.jar"), sourceJar);
+
+    try {
+      final File targetJar = new File(myNewerDir, "lib/empty.jar");
+      if (targetJar.exists()) targetJar.delete();
+      assertTrue(targetJar.createNewFile());
+
+      try {
+        createPatch();
+        fail("Should have failed to create a patch against empty .jar");
+      }
+      catch (IOException e) {
+        final String reason = e.getMessage();
+        assertEquals("Corrupted target file: " + targetJar, reason);
+      }
+      finally {
+        targetJar.delete();
+      }
+    }
+    finally {
+      sourceJar.delete();
+    }
+  }
+
+  @Test
+  public void testReverting() throws Exception {
+    createPatch();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction());
+    assertNothingHasChanged(preparationResult, new HashMap<String, ValidationResult.Option>());
+  }
+
+  @Test
+  public void testApplyingWithAbsentFileToDelete() throws Exception {
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    new File(myOlderDir, "bin/idea.bat").delete();
+
+    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testApplyingWithAbsentOptionalFile() throws Exception {
+    FileUtil.writeToFile(new File(myNewerDir, "bin/idea.bat"), "new content".getBytes());
+
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.singletonList("bin/idea.bat"), TEST_UI);
+
+    new File(myOlderDir, "bin/idea.bat").delete();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    assertTrue(preparationResult.validationResults.isEmpty());
+    assertAppliedAndRevertedCorrectly(preparationResult);
+  }
+
+  @Test
+  public void testRevertingWithAbsentFileToDelete() throws Exception {
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    new File(myOlderDir, "bin/idea.bat").delete();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction());
+    assertNothingHasChanged(preparationResult, new HashMap<String, ValidationResult.Option>());
+  }
+
+  @Test
+  public void testApplyingWithoutCriticalFiles() throws Exception {
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+
+    assertTrue(PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), TEST_UI));
+  }
+
+  @Test
+  public void testApplyingWithCriticalFiles() throws Exception {
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Arrays.asList("lib/annotations.jar"),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+
+    assertTrue(PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), TEST_UI));
+    assertAppliedCorrectly();
+  }
+
+  @Test
+  public void testApplyingWithCaseChangedNames() throws Exception {
+    FileUtil.rename(new File(myOlderDir, "Readme.txt"),
+                    new File(myOlderDir, "README.txt"));
+
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testCreatingAndApplyingWhenDirectoryBecomesFile() throws Exception {
+    File file = new File(myOlderDir, "Readme.txt");
+    file.delete();
+    file.mkdirs();
+
+    new File(file, "subFile.txt").createNewFile();
+    new File(file, "subDir").mkdir();
+    new File(file, "subDir/subFile.txt").createNewFile();
+
+    FileUtil.copy(new File(myOlderDir, "lib/boot.jar"),
+                  new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"));
+
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testCreatingAndApplyingWhenFileBecomesDirectory() throws Exception {
+    File file = new File(myOlderDir, "bin");
+    assertTrue(FileUtil.delete(file));
+    file.createNewFile();
+
+    FileUtil.copy(new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"),
+                  new File(myOlderDir, "lib/boot.jar"));
+
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                            Collections.<String>emptyList(), TEST_UI);
+
+    assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testConsideringOptions() throws Exception {
+    createPatch();
+
+    PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
+    Map<String, ValidationResult.Option> options = new HashMap<String, ValidationResult.Option>();
+    for (PatchAction each : preparationResult.patch.getActions()) {
+      options.put(each.getPath(), ValidationResult.Option.IGNORE);
+    }
+
+    assertNothingHasChanged(preparationResult, options);
+  }
+
+  private void createPatch() throws IOException, OperationCancelledException {
+    PatchFileCreator.create(myOlderDir, myNewerDir, myFile,
+                            Collections.<String>emptyList(), Collections.<String>emptyList(), Collections.<String>emptyList(), TEST_UI);
+    assertTrue(myFile.exists());
+  }
+
+  private void assertNothingHasChanged(PatchFileCreator.PreparationResult preparationResult, Map<String, ValidationResult.Option> options)
+    throws Exception {
+    Map<String, Long> before = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+    PatchFileCreator.apply(preparationResult, options, TEST_UI);
+    Map<String, Long> after = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+
+    DiffCalculator.Result diff = DiffCalculator.calculate(before, after);
+    assertTrue(diff.filesToCreate.isEmpty());
+    assertTrue(diff.filesToDelete.isEmpty());
+    assertTrue(diff.filesToUpdate.isEmpty());
+  }
+
+  private void assertAppliedAndRevertedCorrectly(PatchFileCreator.PreparationResult preparationResult)
+    throws IOException, OperationCancelledException {
+
+    Map<String, Long> original = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
+
+    File backup = getTempFile("backup");
+
+    for (ValidationResult each : preparationResult.validationResults) {
+      assertTrue(each.toString(), each.kind != ValidationResult.Kind.ERROR);
+    }
+
+    List<PatchAction> appliedActions =
+      PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), backup, TEST_UI).appliedActions;
+    assertAppliedCorrectly();
+
+    assertFalse(original.equals(Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI)));
+
+    PatchFileCreator.revert(preparationResult, appliedActions, backup, TEST_UI);
+
+    assertEquals(original, Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI));
+  }
+
+  protected void assertAppliedCorrectly() throws IOException {
+    File newFile = new File(myOlderDir, "newDir/newFile.txt");
+    assertTrue(newFile.exists());
+    assertEquals("hello", FileUtil.loadFile(newFile));
+
+    File changedFile = new File(myOlderDir, "Readme.txt");
+    assertTrue(changedFile.exists());
+    assertEquals("hello", FileUtil.loadFile(changedFile));
+
+    assertFalse(new File(myOlderDir, "bin/idea.bat").exists());
+
+    // do not remove unchanged
+    checkZipEntry("lib/annotations.jar", "org/jetbrains/annotations/Nls.class", 502);
+    // add new
+    checkZipEntry("lib/annotations.jar", "org/jetbrains/annotations/NewClass.class", 453);
+    // update changed
+    checkZipEntry("lib/annotations.jar", "org/jetbrains/annotations/Nullable.class", 546);
+    // remove obsolete
+    checkNoZipEntry("lib/annotations.jar", "org/jetbrains/annotations/TestOnly.class");
+
+    // test for archives with only deleted files
+    checkNoZipEntry("lib/bootstrap.jar", "com/intellij/ide/ClassloaderUtil.class");
+
+    // packing directories too
+    checkZipEntry("lib/annotations.jar", "org/", 0);
+    checkZipEntry("lib/annotations.jar", "org/jetbrains/", 0);
+    checkZipEntry("lib/annotations.jar", "org/jetbrains/annotations/", 0);
+    checkZipEntry("lib/bootstrap.jar", "com/", 0);
+    checkZipEntry("lib/bootstrap.jar", "com/intellij/", 0);
+    checkZipEntry("lib/bootstrap.jar", "com/intellij/ide/", 0);
+  }
+
+  private void checkZipEntry(String jar, String entryName, int expectedSize) throws IOException {
+    ZipFile zipFile = new ZipFile(new File(myOlderDir, jar));
+    try {
+      ZipEntry entry = zipFile.getEntry(entryName);
+      assertNotNull(entry);
+      assertEquals(expectedSize, entry.getSize());
+    }
+    finally {
+      zipFile.close();
+    }
+  }
+
+  private void checkNoZipEntry(String jar, String entryName) throws IOException {
+    ZipFile zipFile = new ZipFile(new File(myOlderDir, jar));
+    try {
+      assertNull(zipFile.getEntry(entryName));
+    }
+    finally {
+      zipFile.close();
+    }
+  }
+
+  private static class MyFailOnApplyPatchAction extends PatchAction {
+    public MyFailOnApplyPatchAction() {
+      super("_dummy_file_", -1);
+    }
+
+    @Override
+    protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected ValidationResult doValidate(File toFile) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    protected void doApply(ZipFile patchFile, File toFile) throws IOException {
+      throw new IOException("dummy exception");
+    }
+
+    @Override
+    protected void doBackup(File toFile, File backupFile) throws IOException {
+    }
+
+    @Override
+    protected void doRevert(File toFile, File backupFile) throws IOException {
+    }
+  }
+}
diff --git a/updater/testSrc/com/intellij/updater/PatchTest.java b/updater/testSrc/com/intellij/updater/PatchTest.java
new file mode 100644
index 0000000..14ff6c0
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/PatchTest.java
@@ -0,0 +1,192 @@
+package com.intellij.updater;
+
+import com.intellij.openapi.util.io.FileUtil;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.nio.channels.FileLock;
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class PatchTest extends PatchTestCase {
+  private Patch myPatch;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                        Collections.<String>emptyList(), TEST_UI);
+  }
+
+  @Test
+  public void testBasics() throws Exception {
+    List<PatchAction> expectedActions = Arrays.asList(
+      new CreateAction("newDir/newFile.txt"),
+      new UpdateAction("Readme.txt", CHECKSUMS.README_TXT),
+      new UpdateZipAction("lib/annotations.jar",
+                          Arrays.asList("org/jetbrains/annotations/NewClass.class"),
+                          Arrays.asList("org/jetbrains/annotations/Nullable.class"),
+                          Arrays.asList("org/jetbrains/annotations/TestOnly.class"),
+                          CHECKSUMS.ANNOTATIONS_JAR),
+      new UpdateZipAction("lib/bootstrap.jar",
+                          Collections.<String>emptyList(),
+                          Collections.<String>emptyList(),
+                          Arrays.asList("com/intellij/ide/ClassloaderUtil.class"),
+                          CHECKSUMS.BOOTSTRAP_JAR),
+      new DeleteAction("bin/idea.bat", CHECKSUMS.IDEA_BAT));
+    List<PatchAction> actualActions = new ArrayList<PatchAction>(myPatch.getActions());
+    Collections.sort(expectedActions, COMPARATOR);
+    Collections.sort(actualActions, COMPARATOR);
+    assertEquals(expectedActions, actualActions);
+  }
+
+  @Test
+  public void testCreatingWithIgnoredFiles() throws Exception {
+    myPatch = new Patch(myOlderDir,
+                        myNewerDir,
+                        Arrays.asList("Readme.txt", "bin/idea.bat"),
+                        Collections.<String>emptyList(),
+                        Collections.<String>emptyList(),
+                        TEST_UI);
+
+    List<PatchAction> expectedActions = Arrays.asList(
+      new CreateAction("newDir/newFile.txt"),
+      new UpdateZipAction("lib/annotations.jar",
+                          Arrays.asList("org/jetbrains/annotations/NewClass.class"),
+                          Arrays.asList("org/jetbrains/annotations/Nullable.class"),
+                          Arrays.asList("org/jetbrains/annotations/TestOnly.class"),
+                          CHECKSUMS.ANNOTATIONS_JAR),
+      new UpdateZipAction("lib/bootstrap.jar",
+                          Collections.<String>emptyList(),
+                          Collections.<String>emptyList(),
+                          Arrays.asList("com/intellij/ide/ClassloaderUtil.class"),
+                          CHECKSUMS.BOOTSTRAP_JAR));
+    List<PatchAction> actualActions = new ArrayList<PatchAction>(myPatch.getActions());
+    Collections.sort(expectedActions, COMPARATOR);
+    Collections.sort(actualActions, COMPARATOR);
+    assertEquals(expectedActions, actualActions);
+  }
+
+  @Test
+  public void testValidation() throws Exception {
+    FileUtil.writeToFile(new File(myOlderDir, "bin/idea.bat"), "changed".getBytes());
+    new File(myOlderDir, "extraDir").mkdirs();
+    new File(myOlderDir, "extraDir/extraFile.txt").createNewFile();
+    new File(myOlderDir, "newDir").mkdirs();
+    new File(myOlderDir, "newDir/newFile.txt").createNewFile();
+    FileUtil.writeToFile(new File(myOlderDir, "Readme.txt"), "changed".getBytes());
+    FileUtil.writeToFile(new File(myOlderDir, "lib/annotations.jar"), "changed".getBytes());
+    FileUtil.delete(new File(myOlderDir, "lib/bootstrap.jar"));
+
+    assertEquals(
+      new HashSet<ValidationResult>(Arrays.asList(
+        new ValidationResult(ValidationResult.Kind.CONFLICT,
+                             "newDir/newFile.txt",
+                             ValidationResult.Action.CREATE,
+                             ValidationResult.ALREADY_EXISTS_MESSAGE,
+                             ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP),
+        new ValidationResult(ValidationResult.Kind.ERROR,
+                             "Readme.txt",
+                             ValidationResult.Action.UPDATE,
+                             ValidationResult.MODIFIED_MESSAGE,
+                             ValidationResult.Option.IGNORE),
+        new ValidationResult(ValidationResult.Kind.ERROR,
+                             "lib/annotations.jar",
+                             ValidationResult.Action.UPDATE,
+                             ValidationResult.MODIFIED_MESSAGE,
+                             ValidationResult.Option.IGNORE),
+        new ValidationResult(ValidationResult.Kind.ERROR,
+                             "lib/bootstrap.jar",
+                             ValidationResult.Action.UPDATE,
+                             ValidationResult.ABSENT_MESSAGE,
+                             ValidationResult.Option.IGNORE),
+        new ValidationResult(ValidationResult.Kind.CONFLICT,
+                             "bin/idea.bat",
+                             ValidationResult.Action.DELETE,
+                             ValidationResult.MODIFIED_MESSAGE,
+                             ValidationResult.Option.DELETE, ValidationResult.Option.KEEP))),
+      new HashSet<ValidationResult>(myPatch.validate(myOlderDir, TEST_UI)));
+  }
+
+  @Test
+  public void testValidationWithOptionalFiles() throws Exception {
+    FileUtil.writeToFile(new File(myOlderDir, "lib/annotations.jar"), "changed".getBytes());
+    assertEquals(new HashSet<ValidationResult>(Arrays.asList(
+      new ValidationResult(ValidationResult.Kind.ERROR,
+                           "lib/annotations.jar",
+                           ValidationResult.Action.UPDATE,
+                           ValidationResult.MODIFIED_MESSAGE,
+                           ValidationResult.Option.IGNORE))),
+                 new HashSet<ValidationResult>(myPatch.validate(myOlderDir, TEST_UI)));
+
+    myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>emptyList(),
+                        Arrays.asList("lib/annotations.jar"), TEST_UI);
+    FileUtil.delete(new File(myOlderDir, "lib/annotations.jar"));
+    assertEquals(Collections.<ValidationResult>emptyList(),
+                 myPatch.validate(myOlderDir, TEST_UI));
+  }
+
+  @Test
+  public void testValidatingNonAccessibleFiles() throws Exception {
+    File f = new File(myOlderDir, "Readme.txt");
+    FileOutputStream s = new FileOutputStream(f, true);
+    try {
+      FileLock lock = s.getChannel().lock();
+      try {
+        List<ValidationResult> result = myPatch.validate(myOlderDir, TEST_UI);
+        assertEquals(
+          new HashSet<ValidationResult>(Arrays.asList(
+            new ValidationResult(ValidationResult.Kind.ERROR,
+                                 "Readme.txt",
+                                 ValidationResult.Action.UPDATE,
+                                 ValidationResult.ACCESS_DENIED_MESSAGE,
+                                 ValidationResult.Option.IGNORE))),
+          new HashSet<ValidationResult>(result));
+      }
+      finally {
+        lock.release();
+      }
+    }
+    finally {
+      s.close();
+    }
+  }
+
+  @Test
+  public void testSaveLoad() throws Exception {
+    File f = getTempFile("file");
+    try {
+      FileOutputStream out = new FileOutputStream(f);
+      try {
+        myPatch.write(out);
+      }
+      finally {
+        out.close();
+      }
+
+      FileInputStream in = new FileInputStream(f);
+      try {
+        assertEquals(myPatch.getActions(), new Patch(in).getActions());
+      }
+      finally {
+        in.close();
+      }
+    }
+    finally {
+      f.delete();
+    }
+  }
+
+  private static final Comparator<PatchAction> COMPARATOR = new Comparator<PatchAction>() {
+    @Override
+    public int compare(PatchAction o1, PatchAction o2) {
+      return o1.toString().compareTo(o2.toString());
+    }
+  };
+}
diff --git a/updater/testSrc/com/intellij/updater/PatchTestCase.java b/updater/testSrc/com/intellij/updater/PatchTestCase.java
new file mode 100644
index 0000000..417c32e
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/PatchTestCase.java
@@ -0,0 +1,41 @@
+package com.intellij.updater;
+
+import com.intellij.openapi.util.io.FileUtil;
+
+import java.io.File;
+
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public abstract class PatchTestCase extends UpdaterTestCase {
+  protected File myNewerDir;
+  protected File myOlderDir;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    myOlderDir = getDataDir();
+    myNewerDir = getTempFile("newDir");
+    FileUtil.copyDir(myOlderDir, myNewerDir);
+
+    FileUtil.delete(new File(myNewerDir, "bin/idea.bat"));
+    FileUtil.writeToFile(new File(myNewerDir, "Readme.txt"), "hello".getBytes());
+    File newFile = new File(myNewerDir, "newDir/newFile.txt");
+    newFile.getParentFile().mkdirs();
+    newFile.createNewFile();
+    FileUtil.writeToFile(newFile, "hello".getBytes());
+
+    FileUtil.delete(new File(myOlderDir, "lib/annotations_changed.jar"));
+    FileUtil.delete(new File(myNewerDir, "lib/annotations.jar"));
+    FileUtil.rename(new File(myNewerDir, "lib/annotations_changed.jar"),
+                    new File(myNewerDir, "lib/annotations.jar"));
+
+    FileUtil.delete(new File(myOlderDir, "lib/bootstrap_deleted.jar"));
+    FileUtil.delete(new File(myNewerDir, "lib/bootstrap.jar"));
+    FileUtil.rename(new File(myNewerDir, "lib/bootstrap_deleted.jar"),
+                    new File(myNewerDir, "lib/bootstrap.jar"));
+
+    FileUtil.delete(new File(myOlderDir, "lib/boot2_changed_with_unchanged_content.jar"));
+    FileUtil.delete(new File(myNewerDir, "lib/boot2.jar"));
+    FileUtil.rename(new File(myNewerDir, "lib/boot2_changed_with_unchanged_content.jar"),
+                    new File(myNewerDir, "lib/boot2.jar"));
+  }
+}
\ No newline at end of file
diff --git a/updater/testSrc/com/intellij/updater/RunnerAdditionalTest.java b/updater/testSrc/com/intellij/updater/RunnerAdditionalTest.java
new file mode 100644
index 0000000..577c2bc
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/RunnerAdditionalTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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 com.intellij.updater;
+
+import junit.framework.TestCase;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.*;
+
+public class RunnerAdditionalTest extends TestCase {
+  private final LinkedList<File> myFiles = new LinkedList<File>();
+  private final ArrayList<NamePair> myFileNames = new ArrayList<NamePair>();
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    myFiles.clear();
+    myFileNames.clear();
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    super.tearDown();
+
+    while (!myFiles.isEmpty()) {
+      remove(myFiles.removeLast());
+    }
+  }
+
+  public void testUpdater1() throws Exception {
+    File dir1 = add(createTempDir("old", ".dir"));
+    File dir2 = add(createTempDir("new", ".dir"));
+    File f3 = add(createFileContent(dir1, "something.txt", "Content version 1"));
+    File f4 = add(createFileContent(dir2, "something.txt", "Content version 2"));
+    File inputJar5 = add(File.createTempFile("input", ".jar"));
+    File tmpPatch6 = add(File.createTempFile("temp", ".patch"));
+    final File patch7 = add(File.createTempFile("output", ".jar"));
+
+    createEmptyJar(inputJar5);
+
+    //noinspection ResultOfMethodCallIgnored
+    patch7.delete();
+    assertFalse(patch7.exists());
+
+    MockUpdaterUI ui1 = new MockUpdaterUI();
+    Runner.createImpl("1.2",                    //oldBuildDesc
+                      "1.3",                    //newBuildDesc
+                      dir1.getAbsolutePath(),   //oldFolder
+                      dir2.getAbsolutePath(),   //newFolder
+                      patch7.getAbsolutePath(), //outPatchJar
+                      tmpPatch6,                //tempPatchFile
+                      new ArrayList<String>(),  //ignoredFiles
+                      new ArrayList<String>(),  //criticalFiles
+                      new ArrayList<String>(),  //optionalFiles
+                      ui1,                      //ui
+                      inputJar5);               //resolvedJar
+
+    assertEquals(
+      "[Start   ] Calculating difference...\n" +
+      "[Status  ] something.txt\n" +
+      "[Status  ] something.txt\n" +
+      "[Start   ] Preparing actions...\n" +
+      "[Status  ] something.txt\n" +
+      "[Start   ] Creating the patch file 'file-6_temp.patch'...\n" +
+      "[Status  ] Packing something.txt\n" +
+      "[Start   ] Packing jar file 'file-7_output.jar'...\n" +
+      "[Start   ] Cleaning up...\n" +
+      "[Indeterminate Progress]\n",
+      ui1.toString());
+    assertTrue(patch7.exists());
+
+    File dir8 = add(createTempDir("extracted", ".dir"));
+    File f9 = add(createFileContent(dir8, "something.txt", "Content version 1"));
+    assertTrue(f9.exists());
+
+    MockUpdaterUI ui2 = new MockUpdaterUI();
+    Runner.doInstallImpl(ui2,
+                         dir8.getAbsolutePath(),
+                         new Runner.IJarResolver() {
+                           @Override
+                           public File resolveJar() throws IOException {
+                             return patch7;
+                           }
+                         });
+    assertEquals(
+      "[Start   ] Extracting patch file...\n" +
+      "[Indeterminate Progress]\n" +
+      "[Start   ] Validating installation...\n" +
+      "[Status  ] something.txt\n" +
+      "[Progress] 100\n" +
+      "[Start   ] Backing up files...\n" +
+      "[Status  ] something.txt\n" +
+      "[Progress] 100\n" +
+      "[Start   ] Applying patch...\n" +
+      "[Status  ] something.txt\n" +
+      "[Progress] 100\n" +
+      "[Start   ] Cleaning up...\n" +
+      "[Indeterminate Progress]\n",
+      ui2.toString());
+    assertEquals("Content version 2", getFileContent(f9));
+  }
+
+  //---- utilities -----
+
+  private File createTempDir(String prefix, String suffix) throws IOException {
+    File d = File.createTempFile(prefix, suffix);
+    if (!d.delete()) throw new IOException("Failed to delete directory " + d.getAbsolutePath());
+    if (!d.mkdirs()) throw new IOException("Failed to mkdirs " + d.getAbsolutePath());
+    return d;
+  }
+
+  private static void createEmptyJar(File jar) throws IOException {
+    FileOutputStream outStream = new FileOutputStream(jar);
+    try {
+      ZipOutputWrapper out = new ZipOutputWrapper(outStream);
+
+      // zip file can't be empty, add one dummy entry
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      baos.write("dummy entry".getBytes("UTF-8"));
+      out.zipBytes("dummy.txt", baos);
+
+      out.finish();
+    } finally {
+      outStream.close();
+    }
+  }
+
+  private File add(File f) {
+    myFiles.add(f);
+
+    String unique = f.getName().replaceAll("[^a-zA-Z.]", "");
+    NamePair fromTo = new NamePair(f.getAbsolutePath(),
+                                   (f.isDirectory() ? "dir-" : "file-") + (myFileNames.size() + 1) + '_' + unique);
+    myFileNames.add(fromTo);
+    Collections.sort(myFileNames);
+    return f;
+  }
+
+  private String getFileContent(File file) throws IOException {
+    if (!file.exists()) throw new IOException("File not found, expected file: " + replaceFileNames(file.getAbsolutePath()));
+    BufferedReader br = new BufferedReader(new FileReader(file));
+    try {
+      return br.readLine();
+    } finally {
+      br.close();
+    }
+  }
+
+  private static File createFileContent(File parentDir, String fileName, String fileContent) throws IOException {
+    File f = new File(parentDir, fileName);
+    OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(f), Charset.forName("UTF-8"));
+    try {
+      fw.write(fileContent);
+    } finally {
+      fw.close();
+    }
+    return f;
+  }
+
+  private static void remove(File... files) {
+    for (File f : files) {
+      if (f != null && f.exists()) {
+        if (!f.delete()) {
+          f.deleteOnExit();
+        }
+      }
+    }
+  }
+
+  private String replaceFileNames(String str) {
+    for (NamePair name : myFileNames) {
+      str = str.replace(name.getFrom(), name.getTo());
+    }
+    return str;
+  }
+
+  /**
+   * A list of from->to name pairs, ordered by descending from
+   * (to get the longer ones first.)
+   */
+  private static class NamePair implements Comparable<NamePair> {
+    private final String myFrom;
+    private final String myTo;
+
+    public NamePair(String from, String to) {
+      myFrom = from;
+      myTo = to;
+    }
+
+    public String getFrom() {
+      return myFrom;
+    }
+
+    public String getTo() {
+      return myTo;
+    }
+
+    @Override
+    public int compareTo(NamePair n2) {
+      return -1 * this.getFrom().compareTo(n2.getFrom());
+    }
+  }
+
+  /**
+   * Mock UpdaterUI that dumps all the text to a string buffer, which can be
+   * grabbed using toString(). It also replaces all the filenames using the
+   * provided name pair list.
+   */
+  private class MockUpdaterUI implements UpdaterUI {
+    private final StringBuilder myOutput = new StringBuilder();
+
+    private MockUpdaterUI() {
+    }
+
+    @Override
+    public void startProcess(String title) {
+      title = replaceFileNames(title);
+      myOutput.append("[Start   ] ").append(title).append('\n');
+    }
+
+    @Override
+    public void setProgress(int percentage) {
+      myOutput.append("[Progress] ").append(percentage).append('\n');
+    }
+
+    @Override
+    public void setProgressIndeterminate() {
+      myOutput.append("[Indeterminate Progress]\n");
+    }
+
+    @Override
+    public void setStatus(String status) {
+      status = replaceFileNames(status);
+      myOutput.append("[Status  ] ").append(status).append('\n');
+    }
+
+    @Override
+    public void showError(Throwable e) {
+      myOutput.append("[Error   ] ").append(e.toString()).append('\n');
+    }
+
+    @Override
+    public void checkCancelled() throws OperationCancelledException {
+      // no-op
+    }
+
+    @Override
+    public Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) throws OperationCancelledException {
+      return Collections.emptyMap();
+    }
+
+    @Override
+    public String toString() {
+      return myOutput.toString();
+    }
+  }
+}
diff --git a/updater/testSrc/com/intellij/updater/RunnerTest.java b/updater/testSrc/com/intellij/updater/RunnerTest.java
new file mode 100644
index 0000000..4813db1
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/RunnerTest.java
@@ -0,0 +1,25 @@
+package com.intellij.updater;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+
+public class RunnerTest extends UpdaterTestCase {
+  @Test
+  public void testExtractingFiles() throws Exception {
+    String[] args = {"bar", "ignored=xxx;yyy;zzz/zzz", "critical=", "ignored=aaa", "baz", "critical=ccc"};
+    Runner.initLogger(System.getProperty("java.io.tmpdir"));
+
+    assertEquals(Arrays.asList("xxx", "yyy", "zzz/zzz", "aaa"),
+                 Runner.extractFiles(args, "ignored"));
+
+    assertEquals(Arrays.asList("ccc"),
+                 Runner.extractFiles(args, "critical"));
+
+    assertEquals(Collections.<String>emptyList(),
+                 Runner.extractFiles(args, "unknown"));
+  }
+}
diff --git a/updater/testSrc/com/intellij/updater/UpdaterTestCase.java b/updater/testSrc/com/intellij/updater/UpdaterTestCase.java
new file mode 100644
index 0000000..82d7bbe
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/UpdaterTestCase.java
@@ -0,0 +1,73 @@
+package com.intellij.updater;
+
+import com.intellij.openapi.application.ex.PathManagerEx;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
+import com.intellij.testFramework.fixtures.TempDirTestFixture;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.File;
+
+public abstract class UpdaterTestCase {
+  protected static final UpdaterUI TEST_UI = new ConsoleUpdaterUI(){
+    @Override
+    public void startProcess(String title) {
+    }
+
+    @Override
+    public void setStatus(String status) {
+    }
+  };
+
+  protected CheckSums CHECKSUMS;
+  private TempDirTestFixture myTempDirFixture;
+
+  @Before
+  public void setUp() throws Exception {
+    Runner.initLogger(System.getProperty("java.io.tmpdir"));
+    myTempDirFixture = IdeaTestFixtureFactory.getFixtureFactory().createTempDirTestFixture();
+    myTempDirFixture.setUp();
+
+    FileUtil.copyDir(PathManagerEx.findFileUnderCommunityHome("updater/testData"), getDataDir());
+
+    boolean windowsLineEnds = new File(getDataDir(), "Readme.txt").length() == 7132;
+    CHECKSUMS = new CheckSums(windowsLineEnds);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    myTempDirFixture.tearDown();
+    Utils.cleanup();
+  }
+
+  public File getDataDir() {
+    return getTempFile("data");
+  }
+
+  public File getTempFile(String fileName) {
+    return new File(myTempDirFixture.getTempDirPath(), fileName);
+  }
+
+  protected static class CheckSums {
+    public final long README_TXT;
+    public final long IDEA_BAT;
+    public final long ANNOTATIONS_JAR;
+    public final long BOOTSTRAP_JAR;
+    public final long FOCUSKILLER_DLL;
+
+    public CheckSums(boolean windowsLineEnds) {
+      if (windowsLineEnds) {
+        README_TXT = 1272723667L;
+        IDEA_BAT = 3088608749L;
+      }
+      else {
+        README_TXT = 7256327L;
+        IDEA_BAT = 1493936069L;
+      }
+      ANNOTATIONS_JAR = 2119442657L;
+      BOOTSTRAP_JAR = 2082851308L;
+      FOCUSKILLER_DLL = 1991212227L;
+    }
+  }
+}
\ No newline at end of file
diff --git a/updater/testSrc/com/intellij/updater/UtilsTest.java b/updater/testSrc/com/intellij/updater/UtilsTest.java
new file mode 100755
index 0000000..6e9e4eb
--- /dev/null
+++ b/updater/testSrc/com/intellij/updater/UtilsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 com.intellij.updater;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class UtilsTest extends TestCase {
+
+  private boolean mIsWindows;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    mIsWindows = Utils.isWindows();
+  }
+
+  public void testDelete() throws Exception {
+    File f = File.createTempFile("test", "tmp");
+    assertTrue(f.exists());
+
+    try {
+      Utils.delete(f);
+      assertFalse(f.exists());
+    } finally {
+      f.deleteOnExit();
+    }
+  }
+
+  public void testDelete_LockedFile() throws Exception {
+    File f = File.createTempFile("test", "tmp");
+    assertTrue(f.exists());
+
+    long millis = 0;
+    FileWriter fw = new FileWriter(f);
+    try {
+      // This locks the file on Windows, preventing it from being deleted.
+      // Utils.delete() will retry for about 100 ms.
+      fw.write("test");
+      millis = System.currentTimeMillis();
+
+      Utils.delete(f);
+
+    } catch (IOException e) {
+      millis = System.currentTimeMillis() - millis;
+      assertEquals("Cannot delete file " + f.getAbsolutePath(), e.getMessage());
+      assertTrue("Utils.delete took " + millis + " ms, which is less than the expected 100 ms.", millis > 100);
+      return;
+
+    } finally {
+      f.deleteOnExit();
+      fw.close();
+    }
+
+    assertFalse("Utils.delete did not fail with the expected IOException on Windows.", mIsWindows);
+  }
+}
diff --git a/updater/testUI/.gitignore b/updater/testUI/.gitignore
new file mode 100644
index 0000000..378eac2
--- /dev/null
+++ b/updater/testUI/.gitignore
@@ -0,0 +1 @@
+build
diff --git a/updater/testUI/build.gradle b/updater/testUI/build.gradle
new file mode 100755
index 0000000..2c86712
--- /dev/null
+++ b/updater/testUI/build.gradle
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.
+ */
+import java.nio.charset.Charset
+
+apply plugin: "application"
+
+project.mainClassName = "com.intellij.updater.Runner"
+
+defaultTasks "testSuite"
+
+repositories {
+  mavenCentral()
+}
+
+dependencies {
+  compile files("../../lib/log4j.jar")
+  testCompile group: "junit", name: "junit", version: "3.+"
+}
+
+sourceSets {
+  main {
+    java      { srcDir "../src" }
+    resources { srcDir "../src" }
+  }
+  test {
+    java      { srcDir "../tests" }
+    resources { srcDir "../tests" }
+  }
+}
+
+
+test {
+  testLogging {
+    showStandardStreams = true
+    showStackTraces = true
+    exceptionFormat = "full"
+  }
+}
+
+//---
+
+// Task : Tests updater create + install in the default normal case.
+// Scope: large test.
+// Running this will display the install patcher UI.
+task testUI1(dependsOn: jar) << {
+  println "## Running UI test 1"
+  println "## Jar file: " + jar.archivePath.getAbsolutePath()
+
+  // Create a temp dir inside the gradle build/tmp dir
+  //noinspection GroovyAssignabilityCheck
+  def buildTmpDir = new File(project.buildDir, "tmp");
+  def tmpDir = _createTempDir(buildTmpDir)
+  def logDir = _createTempDir(buildTmpDir)
+
+  try {
+    // create a "from version 1" and "to version 2" directories
+    def dataDir1 = _createTestData(tmpDir, "1")
+    assert  _mkFile(dataDir1, "plugins", "v1", "myplugin.jar").exists()
+    assert !_mkFile(dataDir1, "plugins", "v2", "myplugin.jar").exists()
+    assert _getFileContent(_mkFile(dataDir1, "build.txt")).equals("AI-123.45678-1")
+
+    def dataDir2 = _createTestData(tmpDir, "2")
+    assert !_mkFile(dataDir2, "plugins", "v1", "myplugin.jar").exists()
+    assert  _mkFile(dataDir2, "plugins", "v2", "myplugin.jar").exists()
+    assert _getFileContent(_mkFile(dataDir2, "build.txt")).equals("AI-123.45678-2")
+
+    def patch    = _createFileContent(tmpDir, "patch.jar", "patch jar placeholder");
+    patch.delete()
+    assert !patch.exists()
+
+    def logFullFile  = _mkFile(logDir, "idea_updater.log")
+    def logErrorFile = _mkFile(logDir, "idea_updater_error.log")
+    assert !logFullFile .exists()
+    assert !logErrorFile.exists()
+
+    // call updater jar to create a diff, resulting in a patch jar.
+    println "## Invoking updater <create>"
+    javaexec {
+      classpath jar.archivePath
+      classpath "../../lib/log4j.jar"
+      main = "com.intellij.updater.Runner"
+      args "create"
+      args "AI-123.45678-1"
+      args "AI-123.45678-2"
+      args dataDir1.getAbsolutePath()
+      args dataDir2.getAbsolutePath()
+      args patch.getAbsolutePath()
+      args logDir.getAbsolutePath()
+    }
+    assert patch.exists()
+
+    assert logFullFile .exists()
+    assert logErrorFile.exists()    // should exist and be empty
+    assert _getFileContent(logErrorFile).equals(null)
+
+
+    // that patch jar is self-executable. use it to update dir1 into dir2 in-place.
+    println "## Invoking updater <install>"
+    javaexec {
+      classpath patch
+      classpath "../../lib/log4j.jar"
+      main = "com.intellij.updater.Runner"
+      args "install"
+      args "--exit0"
+      args dataDir1.getAbsolutePath()
+      args logDir.getAbsolutePath()
+    }
+    // build.txt should have changed to v2
+    assert _getFileContent(_mkFile(dataDir1, "build.txt")).equals("AI-123.45678-2")
+    // plugin v1 should have been replaced by pluging v2 in the dataDir1 directory.
+    assert !_mkFile(dataDir1, "plugins", "v1", "myplugin.jar").exists()
+    assert  _mkFile(dataDir1, "plugins", "v2", "myplugin.jar").exists()
+
+    assert logFullFile .exists()
+    assert logErrorFile.exists()    // should exist and be empty
+    assert _getFileContent(logErrorFile).equals(null)
+
+  } finally {
+    // Cleanup on exit
+    tmpDir.deleteDir()
+    logDir.deleteDir()
+  }
+}
+
+// Task : Tests updater create + install with 2 open files.
+// Scope: large *interactive* test.
+// On Windows, the opened files are locked and can't be deleted/overwritten.
+// On Linux/Mac, they should be writable & deletable.
+//
+// Running this will display the install patcher UI, which will fail at
+// first on Windows. After 5 seconds the open files will be closed.
+// Hitting "retry" in the UI after this point should perform the whole
+// install operation again.
+task testUI2(dependsOn: jar) << {
+  println "## Running UI test 2"
+  println "## Jar file: " + jar.archivePath.getAbsolutePath()
+
+  // Create a temp dir inside the gradle build/tmp dir
+  //noinspection GroovyAssignabilityCheck
+  def buildTmpDir = new File(project.buildDir, "tmp");
+  def tmpDir = _createTempDir(buildTmpDir)
+  def logDir = _createTempDir(buildTmpDir)
+  def closeableFiles = []
+
+  try {
+    // create a "from version 1" and "to version 2" directories
+    def dataDir1 = _createTestData(tmpDir, "1")
+    assert  _mkFile(dataDir1, "plugins", "v1", "myplugin.jar").exists()
+    assert !_mkFile(dataDir1, "plugins", "v2", "myplugin.jar").exists()
+    assert _getFileContent(_mkFile(dataDir1, "build.txt")).equals("AI-123.45678-1")
+
+    def dataDir2 = _createTestData(tmpDir, "2")
+    assert !_mkFile(dataDir2, "plugins", "v1", "myplugin.jar").exists()
+    assert  _mkFile(dataDir2, "plugins", "v2", "myplugin.jar").exists()
+    assert _getFileContent(_mkFile(dataDir2, "build.txt")).equals("AI-123.45678-2")
+
+    def patch    = _createFileContent(tmpDir, "patch.jar", "patch jar placeholder");
+    patch.delete()
+    assert !patch.exists()
+
+    def logFullFile  = _mkFile(logDir, "idea_updater.log")
+    def logErrorFile = _mkFile(logDir, "idea_updater_error.log")
+    assert !logFullFile .exists()
+    assert !logErrorFile.exists()
+
+    // keep a couple files open, preventing them from being deleted or replaced on Windows
+    closeableFiles << _openFile(_mkFile(dataDir1, "build.txt"))
+    closeableFiles << _openFile(_mkFile(dataDir1, "plugins", "v1", "myplugin.jar"))
+
+    // call updater jar to create a diff, resulting in a patch jar.
+    println "## Invoking updater <create>"
+    javaexec {
+      classpath jar.archivePath
+      classpath "../../lib/log4j.jar"
+      main = "com.intellij.updater.Runner"
+      args "create"
+      args "AI-123.45678-1"
+      args "AI-123.45678-2"
+      args dataDir1.getAbsolutePath()
+      args dataDir2.getAbsolutePath()
+      args patch.getAbsolutePath()
+      args logDir.getAbsolutePath()
+    }
+    assert patch.exists()
+
+    assert logFullFile .exists()
+    assert logErrorFile.exists()    // should exist and be empty
+    assert _getFileContent(logErrorFile).equals(null)
+
+    Thread.start {
+      sleep(5 * 1000) // 5 seconds
+      synchronized(closeableFiles) {
+        println "##"
+        println "## Closing pending open files --> now click 'Retry' in Updater UI."
+        println "##"
+        closeableFiles.each { it.close() }
+        closeableFiles.clear()
+      }
+    }
+
+    // that patch jar is self-executable. use it to update dir1 into dir2 in-place.
+    println "## Invoking updater <install>"
+    println "##"
+    println "## Note: on Windows some files will be locked for 5 seconds and the installer"
+    println "##       window should display a 'Retry' button. This code will wait 5 seconds"
+    println "##       then unlock the files, at which point you would click that 'Retry'"
+    println "##       button and the install should continue correctly."
+    println "##"
+
+    javaexec {
+      classpath patch
+      classpath "../../lib/log4j.jar"
+      main = "com.intellij.updater.Runner"
+      args "install"
+      args "--exit0"
+      args dataDir1.getAbsolutePath()
+      args logDir.getAbsolutePath()
+    }
+    // build.txt should have changed to v2
+    assert _getFileContent(_mkFile(dataDir1, "build.txt")).equals("AI-123.45678-2")
+    // plugin v1 should have been replaced by pluging v2 in the dataDir1 directory.
+    assert !_mkFile(dataDir1, "plugins", "v1", "myplugin.jar").exists()
+    assert  _mkFile(dataDir1, "plugins", "v2", "myplugin.jar").exists()
+
+    assert logFullFile .exists()
+    // the log error file should exist and should not be empty on Windows.
+    assert logErrorFile.exists()
+    if (System.getProperty("os.name").startsWith("Windows")) {
+      assert _getFileContent(logErrorFile) != null
+    } else {
+      assert _getFileContent(logErrorFile).equals(null)
+    }
+
+  } finally {
+    // Cleanup on exit
+    synchronized(closeableFiles) {
+      closeableFiles.each { it.close() }
+      closeableFiles.clear()
+    }
+    tmpDir.deleteDir()
+    logDir.deleteDir()
+  }
+}
+
+// Task: Test suite to run both tests above.
+task testSuite(dependsOn: [testUI1, testUI2]) << {
+}
+
+// ---- Helper methods
+
+// Convention: all local helper methods start with an underscore to clearly
+// differentiate them from groovy/gradle methods.
+
+
+// Creates a temp dir with a random name in the build/tmp directory.
+File _createTempDir(File parent) {
+  def d = File.createTempFile("test", "", parent)
+  d.delete()
+  d.mkdirs()
+  return d
+}
+
+// Creates a new directory with the specific name in the specified parent directory.
+File _createDir(File parent, String name) {
+  def d = new File(parent, name)
+  d.mkdirs()
+  return d
+}
+
+// Creates a new file with the specified name, in the specified parent directory with the given UTF-8 content.
+File _createFileContent(File parentDir, String fileName, String fileContent) throws IOException {
+  File f = new File(parentDir, fileName)
+  OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(f), Charset.forName("UTF-8"))
+  try {
+    fw.write(fileContent)
+  } finally {
+    fw.close()
+  }
+  return f
+}
+
+// Opens a file for writing (thus locking it on Windows) but does not close it.
+// Caller should call Closeable.close() on the returned Closeable object.
+Closeable _openFile(File file) throws IOException {
+  if (!file.exists()) throw new IOException("File not found, expected file: " + file.getName())
+  // We need to actually write to the file but not change its content so just read it
+  // then write the same thing back to it, without closing it.
+  String content = _getFileContent(file)
+  FileWriter fw = new FileWriter(file, false /*append*/)
+  fw.append(content)
+  fw.flush()
+  println("## Open file " + file.getName());
+  return fw   // file is not closed
+}
+
+// Returns the first line of the file.
+// Returns null if the file exists and is empty.
+// Throws IOException if file does not exist.
+String _getFileContent(File file) throws IOException {
+  if (!file.exists()) throw new IOException("File not found, expected file: " + file.getName())
+  BufferedReader br = new BufferedReader(new FileReader(file))
+  try {
+    return br.readLine()
+  } finally {
+    br.close()
+  }
+}
+
+// Creates a new File() object with the concatenated name segments.
+File _mkFile(File base, String...segments) {
+  for(String segment : segments) {
+    base = new File(base, segment)
+  }
+  return base
+}
+
+// Creates a mock test data for an idea-based IDE.
+File _createTestData(File tmpDir, String value) {
+  File root     = _createDir(tmpDir,  "idea-ide-" + value)
+  File bin      = _createDir(root,    "bin")
+  File lib      = _createDir(root,    "lib")
+  File plugins  = _createDir(root,    "plugins")
+  File myplugin = _createDir(plugins, "v" + value)
+
+  _createFileContent(root,     "build.txt",    "AI-123.45678-"  + value);
+  _createFileContent(bin,      "idea.exe",     "binary file "   + value);
+  _createFileContent(lib,      "idea.jar",     "some jar file " + value);
+  _createFileContent(myplugin, "myplugin.jar", "some jar file " + value);
+
+  return root
+}
diff --git a/updater/testUI/gradle/wrapper/gradle-wrapper.jar b/updater/testUI/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7b359d7
--- /dev/null
+++ b/updater/testUI/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/updater/testUI/gradle/wrapper/gradle-wrapper.properties b/updater/testUI/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..97f4761
--- /dev/null
+++ b/updater/testUI/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Dec 12 15:16:42 PST 2012
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=../../../../../external/gradle/gradle-1.9-bin.zip
diff --git a/updater/testUI/gradlew b/updater/testUI/gradlew
new file mode 100755
index 0000000..779e68d
--- /dev/null
+++ b/updater/testUI/gradlew
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/"
+APP_HOME="`pwd -P`"
+cd "$SAVED"
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+# Change the project's .gradle to the android out dir.
+ANDROID_GRADLE_ROOT="$APP_HOME/../../../../out/host/gradle/tools/updater"
+if [[ -z "$ANDROID_CACHE_DIR" ]]; then
+  ANDROID_CACHE_DIR="$ANDROID_GRADLE_ROOT/.gradle"
+fi
+
+# Change the local user directories to be under the android out dir
+export GRADLE_USER_HOME="$ANDROID_GRADLE_ROOT/.gradle"
+export M2_HOME="$ANDROID_GRADLE_ROOT/.m2"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" \
+    -classpath "$CLASSPATH" \
+    org.gradle.wrapper.GradleWrapperMain \
+    --project-cache-dir=$ANDROID_CACHE_DIR \
+    "$@"
+
diff --git a/updater/testUI/gradlew.bat b/updater/testUI/gradlew.bat
new file mode 100755
index 0000000..3569a7b
--- /dev/null
+++ b/updater/testUI/gradlew.bat
@@ -0,0 +1,96 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Change the project's .gradle to the android out dir.
+set ANDROID_GRADLE_ROOT=%APP_HOME%\..\..\..\..\out\host\gradle\tools\updater
+set ANDROID_CACHE_DIR=%ANDROID_GRADLE_ROOT%\.gradle
+set GRADLE_USER_HOME=%ANDROID_GRADLE_ROOT%\.gradle
+set M2_HOME=%ANDROID_GRADLE_ROOT%\.m2
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% --project-cache-dir=%ANDROID_CACHE_DIR%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/updater/updater.iml b/updater/updater.iml
old mode 100644
new mode 100755
index c053808..e55b58d
--- a/updater/updater.iml
+++ b/updater/updater.iml
@@ -4,9 +4,13 @@
     <exclude-output />
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
     </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Log4J" level="project" />
+    <orderEntry type="module" module-name="testFramework" scope="TEST" />
+    <orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
   </component>
 </module>