class ProbeAll2HopCircuits(object):
    def __init__(self, state, clock, log_dir, stopped, partitions,
                 this_partition, build_duration, circuit_timeout,
                 circuit_generator, log_chunk_size, max_concurrency):
        """
        state: the txtorcon state object
        clock: this argument is normally the twisted global reactor object but
        unit tests might set this to a clock object which can time travel for faster testing.
        log_dir: the directory to write log files
        stopped: callable to call when done
        partitions: the number of partitions to use for processing the set of circuits
        this_partition: which partition of circuit we will process
        build_duration: build a new circuit every specified duration
        circuit_timeout: circuit build timeout duration
        """
        self.state = state
        self.clock = clock
        self.log_dir = log_dir
        self.stopped = stopped
        self.partitions = partitions
        self.this_partition = this_partition
        self.circuit_life_duration = circuit_timeout
        self.circuit_build_duration = build_duration
        self.circuits = circuit_generator
        self.log_chunk_size = log_chunk_size

        self.semaphore = defer.DeferredSemaphore(max_concurrency)
        self.lazy_tail = defer.succeed(None)
        self.tasks = {}
        self.call_id = None

        # XXX adjust me
        self.result_sink = ResultSink(log_dir, chunk_size=log_chunk_size)

    def now(self):
        return 1000 * time.time()

    def serialize_route(self, route):
        """
        Serialize a route.
        """
        return "%s -> %s" % (route[0].id_hex, route[1].id_hex)

    def build_circuit(self, route):
        """
        Build a tor circuit using the specified path of relays
        and a timeout.
        """
        serialized_route = self.serialize_route(route)

        def circuit_build_success(circuit):
            self.count_success.inc()
            return circuit.close()

        def circuit_build_timeout(f):
            f.trap(CircuitBuildTimedOutError)
            self.count_timeout.inc()
            time_end = self.now()
            self.result_sink.send({
                "time_start": time_start,
                "time_end": time_end,
                "path": serialized_route,
                "status": "timeout"
            })
            return None

        def circuit_build_failure(f):
            self.count_failure.inc()
            time_end = self.now()
            self.result_sink.send({
                "time_start": time_start,
                "time_end": time_end,
                "path": serialized_route,
                "status": "failure"
            })
            return None

        def clean_up(opaque):
            self.tasks.pop(serialized_route)

        time_start = self.now()
        d = self.semaphore.run(build_timeout_circuit, self.state, self.clock,
                               route, self.circuit_life_duration)
        self.tasks[serialized_route] = d
        d.addCallback(circuit_build_success)
        d.addErrback(circuit_build_timeout)
        d.addErrback(circuit_build_failure)
        d.addBoth(clean_up)

    def start(self):
        def pop():
            try:
                route = self.circuits.next()
                log.msg(self.serialize_route(route))
                self.build_circuit(route)
            except StopIteration:
                self.stop()
            else:
                self.call_id = self.clock.callLater(
                    self.circuit_build_duration, pop)

        self.clock.callLater(0, pop)

    def stop(self):
        try:
            if self.call_id is not None:
                self.call_id.cancel()
        except AlreadyCalled:
            pass
        dl = defer.DeferredList(self.tasks.values())
        dl.addCallback(lambda ign: self.result_sink.end_flush())
        dl.addCallback(lambda ign: self.stopped())
        return dl
class TestResultSink(unittest.TestCase):

    def test_send_multiple_chunk_size(self):
        self.tmpdir = mkdtemp()
        chunk_size = 10
        self.result_sink = ResultSink(self.tmpdir, chunk_size=chunk_size)
        test_data = {'test_method': 'test_send_chunk_size'}
        num_chunks = randint(121, 212)
        deferreds = []
        for _ in xrange(chunk_size*num_chunks):
            deferreds += [self.result_sink.send(test_data)]

        def validate(_, dirname, fnames):
            assert len(fnames) == num_chunks
            for fname in fnames:
                path = join(dirname, fname)
                with open(path, 'r') as testfile:
                    results = json.load(testfile)
                    for result in results:
                        assert 'test_method' in result
                        assert result['test_method'] == 'test_send_chunk_size'
        dl = defer.DeferredList(deferreds)
        dl.addCallback(lambda results: self.result_sink.end_flush())
        dl.addCallback(lambda results: walk(self.tmpdir, validate, None))
        return dl

    def test_send_chunk_size(self):
        self.tmpdir = mkdtemp()
        chunk_size = 10
        self.result_sink = ResultSink(self.tmpdir, chunk_size=chunk_size)
        test_data = {'test_method': 'test_send_chunk_size'}
        deferreds = []
        for _ in xrange(chunk_size):
            deferreds += [self.result_sink.send(test_data)]

        def validate(_, dirname, fnames):
            assert len(fnames) == 1
            for fname in fnames:
                path = join(dirname, fname)
                with open(path, 'r') as testfile:
                    results = json.load(testfile)
                    for result in results:
                        assert 'test_method' in result
                        assert result['test_method'] == 'test_send_chunk_size'
        dl = defer.DeferredList(deferreds)
        dl.addCallback(lambda results: self.result_sink.end_flush())
        dl.addCallback(lambda results: walk(self.tmpdir, validate, None))
        return dl

    def test_end_flush(self):
        self.tmpdir = mkdtemp()
        chunk_size = 10
        self.result_sink = ResultSink(self.tmpdir, chunk_size=chunk_size)
        test_data = {'test_method': 'test_send_chunk_size'}
        deferreds = []
        for _ in xrange(chunk_size + 3):
            deferreds += [self.result_sink.send(test_data)]

        def validate(_, dirname, fnames):
            # assert len(fnames) == 1
            for fname in fnames:
                path = join(dirname, fname)
                with open(path, 'r') as testfile:
                    results = json.load(testfile)
                    for result in results:
                        assert 'test_method' in result
                        assert result['test_method'] == 'test_send_chunk_size'
        dl = defer.DeferredList(deferreds)
        dl.addCallback(lambda results: self.result_sink.end_flush())
        dl.addCallback(lambda results: walk(self.tmpdir, validate, None))
        return dl

    def tearDown(self):
        def remove_tree(result):
            rmtree(self.tmpdir)
            return result
        return self.result_sink.current_task.addCallback(remove_tree)