Allow specifying environment variables.

Refactor the code a little to make this easier to munge around.
diff --git a/tools/run_tests/jobset.py b/tools/run_tests/jobset.py
index 8f16a4f..19ae52e 100755
--- a/tools/run_tests/jobset.py
+++ b/tools/run_tests/jobset.py
@@ -86,19 +86,49 @@
   raise Exception('%s not found' % filename)
 
 
+class JobSpec(object):
+  """Specifies what to run for a job."""
+
+  def __init__(self, cmdline, shortname=None, environ={}, hash_targets=[]):
+    """
+    Arguments:
+      cmdline: a list of arguments to pass as the command line
+      environ: a dictionary of environment variables to set in the child process
+      hash_targets: which files to include in the hash representing the jobs version
+                    (or empty, indicating the job should not be hashed)
+    """
+    self.cmdline = cmdline
+    self.environ = environ
+    self.shortname = cmdline[0] if shortname is None else shortname
+    self.hash_targets = hash_targets or []
+
+  def identity(self):
+    return '%r %r %r' % (self.cmdline, self.environ, self.hash_targets)
+
+  def __hash__(self):
+    return hash(self.identity())
+
+  def __cmp__(self, other):
+    return self.identity() == other.identity()
+
+
 class Job(object):
   """Manages one job."""
 
-  def __init__(self, cmdline, bin_hash, newline_on_success):
-    self._cmdline = cmdline
+  def __init__(self, spec, bin_hash, newline_on_success):
+    self._spec = spec
     self._bin_hash = bin_hash
     self._tempfile = tempfile.TemporaryFile()
-    self._process = subprocess.Popen(args=cmdline,
+    env = os.environ.copy()
+    for k, v in spec.environ.iteritems():
+      env[k] = v
+    self._process = subprocess.Popen(args=spec.cmdline,
                                      stderr=subprocess.STDOUT,
-                                     stdout=self._tempfile)
+                                     stdout=self._tempfile,
+                                     env=env)
     self._state = _RUNNING
     self._newline_on_success = newline_on_success
-    message('START', ' '.join(self._cmdline))
+    message('START', spec.shortname)
 
   def state(self, update_cache):
     """Poll current state of the job. Prints messages at completion."""
@@ -108,12 +138,13 @@
         self._tempfile.seek(0)
         stdout = self._tempfile.read()
         message('FAILED', '%s [ret=%d]' % (
-            ' '.join(self._cmdline), self._process.returncode), stdout)
+            self._spec.shortname, self._process.returncode), stdout)
       else:
         self._state = _SUCCESS
-        message('PASSED', '%s' % ' '.join(self._cmdline),
+        message('PASSED', self._spec.shortname,
                 do_newline=self._newline_on_success)
-        update_cache.finished(self._cmdline, self._bin_hash)
+        if self._bin_hash:
+          update_cache.finished(self._spec.identity(), self._bin_hash)
     return self._state
 
   def kill(self):
@@ -135,16 +166,26 @@
     self._newline_on_success = newline_on_success
     self._cache = cache
 
-  def start(self, cmdline):
+  def start(self, spec):
     """Start a job. Return True on success, False on failure."""
     while len(self._running) >= self._maxjobs:
       if self.cancelled(): return False
       self.reap()
     if self.cancelled(): return False
-    with open(which(cmdline[0])) as f:
-      bin_hash = hashlib.sha1(f.read()).hexdigest()
-    if self._cache.should_run(cmdline, bin_hash):
-      self._running.add(Job(cmdline, bin_hash, self._newline_on_success))
+    if spec.hash_targets:
+      bin_hash = hashlib.sha1()
+      for fn in spec.hash_targets:
+        with open(which(fn)) as f:
+          bin_hash.update(f.read())
+      bin_hash = bin_hash.hexdigest()
+      should_run = self._cache.should_run(spec.identity(), bin_hash)
+    else:
+      bin_hash = None
+      should_run = True
+    if should_run:
+      self._running.add(Job(spec,
+                            bin_hash,
+                            self._newline_on_success))
     return True
 
   def reap(self):