[tor-commits] [collector/master] All non-scheduling properties can now be reconfigured at runtime.

karsten at torproject.org karsten at torproject.org
Thu Aug 11 08:44:43 UTC 2016


commit b17f3d2d9c26cad3cbc26680a39e964e23a534a8
Author: iwakeh <iwakeh at torproject.org>
Date:   Tue Aug 2 21:36:07 2016 +0200

    All non-scheduling properties can now be reconfigured at runtime.
    
    Implements #19720:
    Adapted testcoverage and test, restructured Configuration, adapted logging.
---
 build.xml                                          |  12 +--
 src/main/java/org/torproject/collector/Main.java   |  28 ++---
 .../torproject/collector/conf/Configuration.java   | 118 +++++++++++++++++++--
 .../torproject/collector/cron/CollecTorMain.java   |  34 +++++-
 .../org/torproject/collector/cron/Scheduler.java   |   2 +-
 src/main/resources/collector.properties            |   5 +
 src/main/resources/logback.xml                     |   8 ++
 .../java/org/torproject/collector/MainTest.java    |  46 ++++++--
 .../collector/conf/ConfigurationTest.java          |  95 +++++++++++++----
 9 files changed, 280 insertions(+), 68 deletions(-)

diff --git a/build.xml b/build.xml
index e06bb99..75e00f8 100644
--- a/build.xml
+++ b/build.xml
@@ -239,12 +239,12 @@
       </fileset>
     </cobertura-report>
     <cobertura-check totallinerate="5" totalbranchrate="1" >
-      <regex pattern="org.torproject.collector.conf.*" branchrate="100" linerate="100"/>
-      <regex pattern="org.torproject.collector.cron.CollecTorMain"
-             branchrate="50" linerate="63" />
-      <regex pattern="org.torproject.collector.cron.Scheduler"
-             branchrate="75" linerate="82" />
-      <regex pattern="org.torproject.collector.Main" branchrate="66" linerate="94" />
+      <regex pattern="org.torproject.collector.conf"
+             branchrate="87" linerate="100"/>
+      <regex pattern="org.torproject.collector.cron"
+             branchrate="50" linerate="71" />
+      <regex pattern="org.torproject.collector.Main"
+             branchrate="100" linerate="100" />
     </cobertura-check>
   </target>
   <target name="test" depends="compile,compile-tests">
diff --git a/src/main/java/org/torproject/collector/Main.java b/src/main/java/org/torproject/collector/Main.java
index 97c7a0c..b48c3e6 100644
--- a/src/main/java/org/torproject/collector/Main.java
+++ b/src/main/java/org/torproject/collector/Main.java
@@ -21,6 +21,8 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.HashMap;
 import java.util.Map;
@@ -57,20 +59,20 @@ public class Main {
    * See class description {@link Main}.
    */
   public static void main(String[] args) throws Exception {
-    File confFile = null;
+    Path confPath = null;
     if (args == null || args.length == 0) {
-      confFile = new File(CONF_FILE);
+      confPath = Paths.get(CONF_FILE);
     } else if (args.length == 1) {
-      confFile = new File(args[0]);
+      confPath = Paths.get(args[0]);
     } else {
       printUsage("CollecTor takes at most one argument.");
       return;
     }
-    if (!confFile.exists() || confFile.length() < 1L) {
-      writeDefaultConfig(confFile);
+    if (!confPath.toFile().exists() || confPath.toFile().length() < 1L) {
+      writeDefaultConfig(confPath);
       return;
     } else {
-      readConfigurationFrom(confFile);
+      conf.setWatchableSourceAndLoad(confPath);
     }
     Scheduler.getInstance().scheduleModuleRuns(collecTorMains, conf);
   }
@@ -81,10 +83,10 @@ public class Main {
     System.out.println(msg + "\n" + usage);
   }
 
-  private static void writeDefaultConfig(File confFile) {
+  private static void writeDefaultConfig(Path confPath) {
     try {
       Files.copy(Main.class.getClassLoader().getResource(CONF_FILE).openStream(),
-          confFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+          confPath, StandardCopyOption.REPLACE_EXISTING);
       printUsage("Could not find config file. In the default "
           + "configuration, we are not configured to read data from any "
           + "data source or write data to any data sink. You need to "
@@ -93,15 +95,7 @@ public class Main {
           + "Refer to the manual for more information.");
     } catch (IOException e) {
       log.error("Cannot write default configuration. Reason: " + e, e);
-    }
-  }
-
-  private static void readConfigurationFrom(File confFile) throws Exception {
-    try (FileInputStream fis = new FileInputStream(confFile)) {
-      conf.load(fis);
-    } catch (Exception e) { // catch all possible problems
-      log.error("Cannot read configuration. Reason: " + e, e);
-      throw e;
+      throw new RuntimeException(e);
     }
   }
 
diff --git a/src/main/java/org/torproject/collector/conf/Configuration.java b/src/main/java/org/torproject/collector/conf/Configuration.java
index 9295811..205a977 100644
--- a/src/main/java/org/torproject/collector/conf/Configuration.java
+++ b/src/main/java/org/torproject/collector/conf/Configuration.java
@@ -3,21 +3,125 @@
 
 package org.torproject.collector.conf;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.attribute.FileTime;
+import java.util.List;
+import java.util.Observable;
 import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Initialize configuration with defaults from collector.properties,
  * unless a configuration properties file is available.
  */
-public class Configuration extends Properties {
+public class Configuration extends Observable implements Cloneable {
+
+  private static final Logger log = LoggerFactory.getLogger(Configuration.class);
+
+  private final ScheduledExecutorService scheduler =
+      Executors.newScheduledThreadPool(1);
 
   public static final String FIELDSEP = ",";
   public static final String ARRAYSEP = ";";
 
+  private final Properties props = new Properties();
+  private Path configurationFile;
+  private FileTime ft;
+
+  /**
+   * Load the configuration from the given path and start monitoring changes.
+   * If the file was changed, re-read and inform all observers.
+   */
+  public void setWatchableSourceAndLoad(final Path confPath) throws
+      ConfigurationException {
+    this.configurationFile = confPath;
+    try {
+      ft = Files.getLastModifiedTime(confPath);
+      reload();
+    } catch (IOException e) {
+      throw new ConfigurationException("Cannot watch configuration file. "
+          + "Reason: " + e.getMessage(), e);
+    }
+    this.scheduler.scheduleAtFixedRate(new Runnable() {
+        public void run() {
+          log.trace("Check configuration file.");
+            try {
+              FileTime ftNow = Files.getLastModifiedTime(confPath);
+              if (ft.compareTo(ftNow) < 0) {
+                log.info("Configuration file was changed.");
+                reload();
+                setChanged();
+                notifyObservers(null);
+              }
+              ft = ftNow;
+            } catch (IOException | RuntimeException re) {
+              log.error("Cannot reload configuration file.", re);
+            }
+        }
+      }, 1, 1, TimeUnit.MINUTES);
+  }
+
+  private final void reload() throws IOException {
+    props.clear();
+    try (FileInputStream fis
+        = new FileInputStream(configurationFile.toFile())) {
+      props.load(fis);
+    }
+  }
+
+  /** Return a copy of all properties. */
+  public Properties getPropertiesCopy() {
+    return (Properties) props.clone();
+  }
+
+  /**
+   * Loads properties from the given stream.
+   */
+  public void load(InputStream fis) throws IOException {
+    props.load(fis);
+  }
+
+  /** Retrieves the value for key. */
+  public String getProperty(String key) {
+    return props.getProperty(key);
+  }
+
+  /** Sets the value for key. */
+  public void setProperty(String key, String value) {
+    props.setProperty(key, value);
+  }
+
+  /** clears all properties. */
+  public void clear() {
+    props.clear();
+  }
+
+  /** Add all given properties. */
+  public void putAll(Properties allProps) {
+    props.putAll(allProps);
+  }
+
+  /** Count of properties. */
+  public int size() {
+    return props.size();
+  }
+
   /**
    * Returns {@code String[][]} from a property. Commas seperate array elements
    * and semicolons separate arrays, e.g.,
@@ -26,7 +130,7 @@ public class Configuration extends Properties {
   public String[][] getStringArrayArray(Key key) throws ConfigurationException {
     try {
       checkClass(key, String[][].class);
-      String[] interim = getProperty(key.name()).split(ARRAYSEP);
+      String[] interim = props.getProperty(key.name()).split(ARRAYSEP);
       String[][] res = new String[interim.length][];
       for (int i = 0; i < interim.length; i++) {
         res[i] = interim[i].trim().split(FIELDSEP);
@@ -49,7 +153,7 @@ public class Configuration extends Properties {
   public String[] getStringArray(Key key) throws ConfigurationException {
     try {
       checkClass(key, String[].class);
-      String[] res = getProperty(key.name()).split(FIELDSEP);
+      String[] res = props.getProperty(key.name()).split(FIELDSEP);
       for (int i = 0; i < res.length; i++) {
         res[i] = res[i].trim();
       }
@@ -74,7 +178,7 @@ public class Configuration extends Properties {
   public boolean getBool(Key key) throws ConfigurationException {
     try {
       checkClass(key, Boolean.class);
-      return Boolean.parseBoolean(getProperty(key.name()));
+      return Boolean.parseBoolean(props.getProperty(key.name()));
     } catch (RuntimeException re) {
       throw new ConfigurationException("Corrupt property: " + key
           + " reason: " + re.getMessage(), re);
@@ -89,7 +193,7 @@ public class Configuration extends Properties {
   public int getInt(Key key) throws ConfigurationException {
     try {
       checkClass(key, Integer.class);
-      String prop = getProperty(key.name());
+      String prop = props.getProperty(key.name());
       if ("inf".equals(prop)) {
         return Integer.MAX_VALUE;
       } else {
@@ -108,7 +212,7 @@ public class Configuration extends Properties {
   public Path getPath(Key key) throws ConfigurationException {
     try {
       checkClass(key, Path.class);
-      return Paths.get(getProperty(key.name()));
+      return Paths.get(props.getProperty(key.name()));
     } catch (RuntimeException re) {
       throw new ConfigurationException("Corrupt property: " + key
           + " reason: " + re.getMessage(), re);
@@ -122,7 +226,7 @@ public class Configuration extends Properties {
   public URL getUrl(Key key) throws ConfigurationException {
     try {
       checkClass(key, URL.class);
-      return new URL(getProperty(key.name()));
+      return new URL(props.getProperty(key.name()));
     } catch (MalformedURLException mue) {
       throw new ConfigurationException("Corrupt property: " + key
           + " reason: " + mue.getMessage(), mue);
diff --git a/src/main/java/org/torproject/collector/cron/CollecTorMain.java b/src/main/java/org/torproject/collector/cron/CollecTorMain.java
index 83d1ec3..34d711a 100644
--- a/src/main/java/org/torproject/collector/cron/CollecTorMain.java
+++ b/src/main/java/org/torproject/collector/cron/CollecTorMain.java
@@ -16,27 +16,45 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Calendar;
 import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
-public abstract class CollecTorMain implements Runnable {
+public abstract class CollecTorMain implements Observer, Runnable {
 
   private static Logger log = LoggerFactory.getLogger(CollecTorMain.class);
 
   private static final long LIMIT_MB = 200;
 
-  protected Configuration config;
+  private final AtomicBoolean newConfigAvailable = new AtomicBoolean(false);
+
+  protected Configuration config = new Configuration();
+
+  private Configuration newConfig;
 
   public CollecTorMain(Configuration conf) {
-    this.config = conf;
+    this.config.putAll(conf.getPropertiesCopy());
+    conf.addObserver(this);
   }
 
   /**
-   * Log errors preventing successful completion of the module.
+   * Log all errors preventing successful completion of the module.
    */
   @Override
   public final void run() {
+    synchronized (this) {
+      if (newConfigAvailable.get()) {
+        log.info("Module {} received new configuration.", module());
+        synchronized (newConfig) {
+          config.clear();
+          config.putAll(newConfig.getPropertiesCopy());
+          newConfigAvailable.set(false);
+        }
+      }
+    }
     log.info("Starting {} module of CollecTor.", module());
     try {
       startProcessing();
@@ -46,6 +64,14 @@ public abstract class CollecTorMain implements Runnable {
     log.info("Terminating {} module of CollecTor.", module());
   }
 
+  @Override
+  public synchronized void update(Observable obs, Object obj) {
+    newConfigAvailable.set(true);
+    if (obs instanceof Configuration) {
+      newConfig = (Configuration) obs;
+    }
+  }
+
   /**
    * Module specific code goes here.
    */
diff --git a/src/main/java/org/torproject/collector/cron/Scheduler.java b/src/main/java/org/torproject/collector/cron/Scheduler.java
index 66fd7be..85cde5d 100644
--- a/src/main/java/org/torproject/collector/cron/Scheduler.java
+++ b/src/main/java/org/torproject/collector/cron/Scheduler.java
@@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
 /**
  * Scheduler that starts the modules configured in collector.properties.
  */
-public class Scheduler implements ThreadFactory {
+public final class Scheduler implements ThreadFactory {
 
   public static final String ACTIVATED = "Activated";
   public static final String PERIODMIN = "PeriodMinutes";
diff --git a/src/main/resources/collector.properties b/src/main/resources/collector.properties
index 9ba4c3e..0999fcb 100644
--- a/src/main/resources/collector.properties
+++ b/src/main/resources/collector.properties
@@ -1,6 +1,9 @@
 ######## Collector Properties
 #
 ######## Run Configuration ########
+## This part of the configuration cannot be updated at runtime!
+## Changes require a restart in order to become effective.
+##
 # If RunOnce=true, the activated modules below will only be
 # run one time and without any delay.
 # Make sure only to run non-interfering modules together.
@@ -35,6 +38,8 @@ UpdateindexActivated = false
 UpdateindexPeriodMinutes = 2
 # offset in minutes since the epoch and
 UpdateindexOffsetMinutes = 0
+##########################################
+## All below can be changed at runtime.
 ######## General Properties ########
 InstanceBaseUrl = "https://collector.torproject.org"
 LockFilePath = lock
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index f31cf74..2d8a1ce 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -126,6 +126,14 @@
     <appender-ref ref="FILEUPDATEINDEX" />
   </logger>
 
+  <logger name="org.torproject.collector.conf" >
+    <appender-ref ref="FILEBRIDGEDESCS" />
+    <appender-ref ref="FILEEXITLISTS" />
+    <appender-ref ref="FILERELAYDESCS" />
+    <appender-ref ref="FILETORPERF" />
+    <appender-ref ref="FILEUPDATEINDEX" />
+  </logger>
+
   <logger name="org.torproject.collector.cron" >
     <appender-ref ref="FILEBRIDGEDESCS" />
     <appender-ref ref="FILEEXITLISTS" />
diff --git a/src/test/java/org/torproject/collector/MainTest.java b/src/test/java/org/torproject/collector/MainTest.java
index 6b90978..10a6958 100644
--- a/src/test/java/org/torproject/collector/MainTest.java
+++ b/src/test/java/org/torproject/collector/MainTest.java
@@ -9,6 +9,8 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import org.torproject.collector.conf.Key;
+import org.torproject.collector.conf.Configuration;
+import org.torproject.collector.conf.ConfigurationException;
 import org.torproject.collector.cron.Scheduler;
 
 import org.junit.Rule;
@@ -19,39 +21,50 @@ import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.File;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Properties;
-import java.util.Random;
 
 public class MainTest {
 
-  private Random randomSource = new Random();
-
   @Rule
   public TemporaryFolder tmpf = new TemporaryFolder();
 
-  @Test(expected = IOException.class)
+  @Test(expected = ConfigurationException.class)
   public void testInitializationConfigException() throws Exception {
     File conf = new File(Main.CONF_FILE);
-    assertFalse("Please remove " + Main.CONF_FILE + " before running tests!", conf.exists());
+    checkCleanEnv(conf);
     Main.main(new String[] {"/tmp/"});
     assertTrue(conf.exists());
     assertTrue(conf.delete());
   }
 
+  private void checkCleanEnv(File conf) {
+    assertFalse("Please remove " + Main.CONF_FILE + " before running tests!",
+        conf.exists());
+  }
+
   @Test()
   public void testInitializationNullArgs() throws Exception {
     File conf = new File(Main.CONF_FILE);
-    assertFalse("Please remove " + Main.CONF_FILE + " before running tests!", conf.exists());
+    checkCleanEnv(conf);
     Main.main(null);
     assertTrue(conf.exists());
     assertTrue(conf.delete());
   }
 
+  @Test(expected = RuntimeException.class)
+  public void testInitializationUnwritable() throws Exception {
+    File conf = tmpf.newFolder("folder");
+
+    Main.main(new String[] {
+        Paths.get(conf.toString(), "x", "y", "z").toString()});
+  }
+
   @Test()
   public void testInitializationEmptyArgs() throws Exception {
     File conf = new File(Main.CONF_FILE);
-    assertFalse("Please remove " + Main.CONF_FILE + " before running tests!", conf.exists());
+    checkCleanEnv(conf);
     Main.main(new String[] { });
     assertTrue(conf.exists());
     assertTrue(conf.delete());
@@ -60,7 +73,7 @@ public class MainTest {
   @Test()
   public void testInitializationTooManyArgs() throws Exception {
     File conf = new File(Main.CONF_FILE);
-    assertFalse("Please remove " + Main.CONF_FILE + " before running tests!", conf.exists());
+    checkCleanEnv(conf);
     Main.main(new String[] { "x", "y" });
     assertFalse(conf.exists());
   }
@@ -72,12 +85,23 @@ public class MainTest {
     assertEquals(0L, conf.length());
     Main.main(new String[]{conf.toString()});
     assertTrue(4_000L <= conf.length());
-    changeFilePathsAndSetActivation(conf, lockPath, "TorperfActivated");
+    changeFilePathsAndSetActivation(conf, lockPath,
+        Key.TorperfActivated.name());
     Main.main(new String[]{conf.toString()});
-    for(int t = 0; t < 1_000_000; t++) { }
+    waitSec(2);
+  }
+
+  public static void waitSec(int sec) {
+    long now = System.currentTimeMillis();
+    while (System.currentTimeMillis() - now < 1_000L * sec) {
+      try {
+        Thread.sleep(sec * 1_000L);
+      } catch (Exception e) {/* ignored */}
+    }
   }
 
-  private void changeFilePathsAndSetActivation(File f, File l, String a) throws Exception {
+  private void changeFilePathsAndSetActivation(File f, File l, String a)
+      throws Exception {
     List<String> lines = Files.readAllLines(f.toPath());
     BufferedWriter bw = Files.newBufferedWriter(f.toPath());
     File in = tmpf.newFolder();
diff --git a/src/test/java/org/torproject/collector/conf/ConfigurationTest.java b/src/test/java/org/torproject/collector/conf/ConfigurationTest.java
index 2055ae1..14033dc 100644
--- a/src/test/java/org/torproject/collector/conf/ConfigurationTest.java
+++ b/src/test/java/org/torproject/collector/conf/ConfigurationTest.java
@@ -7,17 +7,29 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.torproject.collector.MainTest;
+
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.nio.file.Paths;
 import java.util.Arrays;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Properties;
 import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class ConfigurationTest {
 
   private Random randomSource = new Random();
 
+  @Rule
+  public TemporaryFolder tmpf = new TemporaryFolder();
+
   @Test()
   public void testKeyCount() throws Exception {
     assertEquals("The number of properties keys in enum Key changed."
@@ -28,9 +40,14 @@ public class ConfigurationTest {
   @Test()
   public void testConfiguration() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("TorperfOutputDirectory = xyz".getBytes()));
+    String val = "xyz";
+    conf.setProperty(Key.TorperfOutputDirectory.name(), val);
     assertEquals(1, conf.size());
-    assertEquals("xyz", conf.getProperty("TorperfOutputDirectory"));
+    assertEquals(val, conf.getProperty(Key.TorperfOutputDirectory.name()));
+  }
+
+  private String propLine(Key key, String val) {
+    return key.name() + " = " + val + "\n";
   }
 
   @Test()
@@ -41,14 +58,16 @@ public class ConfigurationTest {
     }
     String[] arrays = new String[] {
       Arrays.toString(array).replace("[", "").replace("]", ""),
-      Arrays.toString(array).replace("[", "").replace("]", "").replaceAll(" ", "")
+      Arrays.toString(array).replace("[", "").replace("]", "")
+          .replaceAll(" ", "")
     };
     Configuration conf = new Configuration();
     for(String input : arrays) {
       conf.clear();
-      conf.load(new ByteArrayInputStream(("CachedRelayDescriptorsDirectories = " + input).getBytes()));
+      conf.setProperty(Key.CachedRelayDescriptorsDirectories.name(), input);
       assertArrayEquals("expected " + Arrays.toString(array) + "\nreceived: "
-          + Arrays.toString(conf.getStringArray(Key.CachedRelayDescriptorsDirectories)),
+          + Arrays.toString(conf
+              .getStringArray(Key.CachedRelayDescriptorsDirectories)),
           array, conf.getStringArray(Key.CachedRelayDescriptorsDirectories));
     }
   }
@@ -56,9 +75,9 @@ public class ConfigurationTest {
   @Test()
   public void testBoolValues() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream(("CompressRelayDescriptorDownloads=false"
-        + "\nImportDirectoryArchives = trUe"
-        + "\nReplaceIpAddressesWithHashes= false").getBytes()));
+    conf.setProperty(Key.CompressRelayDescriptorDownloads.name(), "false");
+    conf.setProperty(Key.ImportDirectoryArchives.name(), "trUe");
+    conf.setProperty(Key.ReplaceIpAddressesWithHashes.name(), "false");
     assertFalse(conf.getBool(Key.CompressRelayDescriptorDownloads));
     assertTrue(conf.getBool(Key.ImportDirectoryArchives));
     assertFalse(conf.getBool(Key.ReplaceIpAddressesWithHashes));
@@ -67,12 +86,13 @@ public class ConfigurationTest {
   @Test()
   public void testIntValues() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("BridgeDescriptorMappingsLimit = inf".getBytes()));
+    conf.setProperty(Key.BridgeDescriptorMappingsLimit.name(), "inf");
     assertEquals(Integer.MAX_VALUE,
         conf.getInt(Key.BridgeDescriptorMappingsLimit));
     int r = randomSource.nextInt(Integer.MAX_VALUE);
     conf.clear();
-    conf.load(new ByteArrayInputStream(("BridgeDescriptorMappingsLimit =" + r).getBytes()));
+    conf.load(new ByteArrayInputStream(
+        propLine(Key.BridgeDescriptorMappingsLimit, "" + r).getBytes()));
     assertEquals(r,
         conf.getInt(Key.BridgeDescriptorMappingsLimit));
    }
@@ -83,8 +103,9 @@ public class ConfigurationTest {
     Configuration conf = new Configuration();
     for(String file : files) {
       conf.clear();
-      conf.load(new ByteArrayInputStream(("DirectoryArchivesOutputDirectory = " + file).getBytes()));
-      assertEquals(new File(file), conf.getPath(Key.DirectoryArchivesOutputDirectory).toFile());
+      conf.setProperty(Key.DirectoryArchivesOutputDirectory.name(), file);
+      assertEquals(new File(file),
+          conf.getPath(Key.DirectoryArchivesOutputDirectory).toFile());
     }
   }
 
@@ -94,52 +115,82 @@ public class ConfigurationTest {
       new String[]{"localsource", "http://127.0.0.1:12345"},
       new String[]{"somesource", "https://some.host.org:12345"}};
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream(("TorperfSources = "
-        + Arrays.deepToString(sourceStrings)).replace("[[", "").replace("]]", "")
-            .replace("], [", Configuration.ARRAYSEP).getBytes()));
-    assertArrayEquals(sourceStrings, conf.getStringArrayArray(Key.TorperfSources));
+    conf.setProperty(Key.TorperfSources.name(),
+        Arrays.deepToString(sourceStrings).replace("[[", "").replace("]]", "")
+            .replace("], [", Configuration.ARRAYSEP));
+    assertArrayEquals(sourceStrings,
+       conf.getStringArrayArray(Key.TorperfSources));
   }
 
   @Test(expected = ConfigurationException.class)
   public void testArrayArrayValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("CachedRelayDescriptorsDirectories".getBytes()));
+    conf.setProperty(Key.CachedRelayDescriptorsDirectories.name(), "");
     conf.getStringArrayArray(Key.TorperfOutputDirectory);
   }
 
   @Test(expected = ConfigurationException.class)
   public void testArrayValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("CachedRelayDescriptorsDirectories".getBytes()));
+    conf.setProperty(Key.CachedRelayDescriptorsDirectories.name(), "");
     conf.getStringArray(Key.TorperfSources);
   }
 
   @Test(expected = ConfigurationException.class)
   public void testBoolValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("TorperfSource = http://x.y.z".getBytes()));
+    conf.setProperty(Key.TorperfSources.name(), "http://x.y.z");
     conf.getBool(Key.CachedRelayDescriptorsDirectories);
   }
 
   @Test(expected = ConfigurationException.class)
   public void testPathValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("DirectoryArchivesDirectory = \\u0000:".getBytes()));
+    conf.setProperty(Key.DirectoryArchivesDirectory.name(), "\\\u0000:");
     conf.getPath(Key.DirectoryArchivesDirectory);
   }
 
   @Test(expected = ConfigurationException.class)
   public void testUrlValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("ExitlistUrl = xxx://y.y.y".getBytes()));
+    conf.setProperty(Key.ExitlistUrl.name(), "xxx://y.y.y");
     conf.getUrl(Key.ExitlistUrl);
   }
 
   @Test(expected = ConfigurationException.class)
   public void testIntValueException() throws Exception {
     Configuration conf = new Configuration();
-    conf.load(new ByteArrayInputStream("BridgeDescriptorMappingsLimit = y7".getBytes()));
+    conf.setProperty(Key.BridgeDescriptorMappingsLimit.name(), "y7");
     conf.getInt(Key.BridgeDescriptorMappingsLimit);
   }
 
+  @Test(expected = ConfigurationException.class)
+  public void testSetWatchableSourceAndLoad() throws Exception {
+    Configuration conf = new Configuration();
+    conf.setWatchableSourceAndLoad(Paths.get("/tmp/phantom.path"));
+  }
+
+    @Test()
+  public void testConfigChange() throws Exception {
+    Configuration conf = new Configuration();
+    final AtomicBoolean called = new AtomicBoolean(false);
+    conf.addObserver(new Observer() {
+        public void update(Observable obs, Object obj) {
+          called.set(true);
+        }
+      });
+    File confFile = tmpf.newFile("empty");
+    conf.setWatchableSourceAndLoad(confFile.toPath());
+    confFile.setLastModified(System.currentTimeMillis());
+    MainTest.waitSec(90);
+    assertTrue("Update was not called.", called.get());
+    called.set(false);
+    MainTest.waitSec(60);
+    assertFalse("Update was called.", called.get());
+    confFile.delete();
+    tmpf.newFolder("empty");
+    MainTest.waitSec(60);
+    assertEquals(0, conf.size());
+  }
+
 }





More information about the tor-commits mailing list