commit 9b06da8803e989b70210f27ac97226abf2f3c494
Author: Arturo Filastò <arturo(a)filasto.net>
Date: Tue Oct 11 18:02:23 2016 +0200
Add two new unittests that ensure the scheduler workflow is working
* Also ensures that a deck is run twice by the scheduler
---
ooni/tests/test_scheduler.py | 260 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 259 insertions(+), 1 deletion(-)
diff --git a/ooni/tests/test_scheduler.py b/ooni/tests/test_scheduler.py
index c8eaf55..e954668 100644
--- a/ooni/tests/test_scheduler.py
+++ b/ooni/tests/test_scheduler.py
@@ -1,13 +1,24 @@
import os
import sys
+import json
import shutil
import random
import tempfile
+import mock
+
+from datetime import datetime, timedelta
+
from twisted.internet import reactor, task, defer
from twisted.trial import unittest
-from ooni.agent.scheduler import ScheduledTask, DidNotRun, FileSystemlockAndMutex
+from ooni.utils import generate_filename, randomSTR, randomDate, LONG_DATE
+from ooni.tests.bases import ConfigTestCase
+
+from ooni.deck.store import DeckStore
+from ooni.agent.scheduler import ScheduledTask, DidNotRun
+from ooni.agent.scheduler import FileSystemlockAndMutex
+from ooni.agent.scheduler import SchedulerService
class TestScheduler(unittest.TestCase):
def test_scheduled_task(self):
@@ -112,3 +123,250 @@ class TestScheduler(unittest.TestCase):
self.assertFalse(lock.locked)
shutil.rmtree(lock_dir)
+
+def random_measurement_name(start_date=None, end_date=None):
+ # By default we use as start date something in the past 6 days and end
+ # date today.
+ if start_date is None:
+ start_date = datetime.now() - timedelta(days=6)
+ if end_date is None:
+ end_date = datetime.now()
+
+ test_details = dict(
+ test_name=random.choice(['http_invalid_request_line',
+ 'web_connectivity',
+ 'http_header_field_manipulation',
+ 'vanilla_tor',
+ 'new_test_name']),
+ probe_cc=randomSTR(2, num=False), # XXX this should be a valid CC
+ probe_asn='AS'+str(random.randint(0, 2**16)),
+ test_start_time=randomDate(start_date, end_date).strftime(LONG_DATE)
+ )
+ return generate_filename(test_details)
+
+def get_measurement_header():
+ return {
+ 'probe_asn': 'AS'+str(random.randint(0, 2**16)),
+ 'probe_cc': randomSTR(2, num=False),
+ 'probe_ip': '127.0.0.1',
+ 'probe_city': None,
+ 'software_name': 'ooniprobe',
+ 'software_version': '0.0.0',
+ 'options': {},
+ 'annotations': {},
+ 'data_format_version': '0.2.0',
+ 'test_name': 'dummy',
+ 'test_version': '0.0.0',
+ 'test_helpers': {},
+ 'test_start_time': '2016-01-01 01:01:01',
+ 'test_runtime': 0.1,
+ 'input_hashes': [],
+ 'report_id': randomSTR(100),
+ 'test_keys': {},
+ 'input': ''
+ }
+
+def write_dummy_measurements(fh, size=100):
+ """
+ :param fh: an open file handle
+ :param size: size of the measurements in bytes to write
+ :return: The actual size that has been written.
+ """
+ written_size = 0
+ while written_size < size:
+ entry = get_measurement_header()
+ entry['test_keys']['data'] = randomSTR(int(size / 10))
+ data = json.dumps(entry)
+ written_size += len(data)
+ fh.write(data)
+ return written_size
+
+
+DUMMY_WEB_FULL = """
+---
+name: Full Web test deck
+description: This deck runs HTTP Header Field Manipulation, HTTP Invalid
+ Request and the Web Connectivity test
+schedule: "@daily"
+tasks:
+- name: Runs the HTTP Header Field Manipulation test
+ ooni:
+ test_name: http_header_field_manipulation
+"""
+
+class TestSchedulerService(ConfigTestCase):
+ def setUp(self):
+ super(TestSchedulerService, self).setUp()
+
+ self.config_patcher = mock.patch('ooni.agent.scheduler.config')
+ self.config_m_patcher = mock.patch('ooni.measurements.config')
+ self.config_mock = self.config_patcher.start()
+ self.config_m_mock = self.config_m_patcher.start()
+
+ self.scheduler_directory = tempfile.mkdtemp()
+ self.config_mock.scheduler_directory = self.scheduler_directory
+
+ self.running_path = tempfile.mkdtemp()
+ self.config_mock.running_path = self.running_path
+
+ self.config_mock.is_initialized.return_value = True
+
+ self.config_mock.basic.measurement_quota = '100M'
+
+ self.measurements_directory = tempfile.mkdtemp()
+ self.config_mock.measurements_directory = self.measurements_directory
+ self.create_dummy_measurements()
+
+ self.config_m_mock.measurements_directory = self.measurements_directory
+
+ self.decks_enabled_directory = tempfile.mkdtemp()
+ self.decks_available_directory = tempfile.mkdtemp()
+
+ self.deck_store = DeckStore(
+ available_directory=self.decks_available_directory,
+ enabled_directory=self.decks_enabled_directory
+ )
+
+ self.mock_deck = mock.MagicMock()
+ self.deck_store.get = lambda deck_id: self.mock_deck
+ self.mock_deck.setup.return_value = defer.succeed(None)
+ self.mock_deck.run.return_value = defer.succeed(None)
+
+ with open(os.path.join(self.decks_available_directory,
+ 'web-full.yaml'), 'w') as out_file:
+ out_file.write(DUMMY_WEB_FULL)
+
+ def create_dummy_measurements(self, count=10, size=10*1024):
+ for _ in range(count):
+ dir_path = os.path.join(
+ self.measurements_directory,
+ random_measurement_name()
+ )
+ os.mkdir(dir_path)
+ with open(os.path.join(dir_path, "measurements.njson"), 'w') as fh:
+ write_dummy_measurements(fh, float(size) / float(count))
+
+ def tearDown(self):
+ super(TestSchedulerService, self).tearDown()
+
+ shutil.rmtree(self.measurements_directory)
+ shutil.rmtree(self.scheduler_directory)
+ shutil.rmtree(self.running_path)
+
+ self.config_patcher.stop()
+ self.config_m_patcher.stop()
+
+ @mock.patch('ooni.agent.scheduler.resources')
+ @mock.patch('ooni.agent.scheduler.probe_ip')
+ @mock.patch('ooni.agent.scheduler.input_store')
+ @mock.patch('ooni.agent.scheduler.oonireport')
+ def test_deck_run_twice(self, mock_resources, mock_probe_ip,
+ mock_input_store, mock_oonireport):
+ mock_probe_ip.geodata['countrycode'] = 'ZZ'
+ mock_probe_ip.lookup.return_value = defer.succeed(None)
+ mock_probe_ip.resolveGeodata.return_value = defer.succeed(None)
+
+ mock_resources.check_for_update.return_value = defer.succeed(None)
+
+ mock_input_store.update.return_value = defer.succeed(None)
+
+ mock_oonireport.upload_all.return_value = defer.succeed(None)
+
+ mock_director = mock.MagicMock()
+ d = defer.Deferred()
+ with mock.patch('ooni.agent.scheduler.deck_store', self.deck_store):
+
+ dummy_clock = task.Clock()
+ scheduler_service = SchedulerService(
+ director=mock_director,
+ _reactor=dummy_clock
+ )
+ scheduler_service.startService()
+ dummy_clock.advance(30)
+
+ now_time = datetime.utcnow()
+ DT_FRMT = "%Y-%m-%dT%H:%M:%SZ"
+
+ for t in scheduler_service._scheduled_tasks:
+ with open(os.path.join(self.scheduler_directory,
+ t.identifier)) as in_file:
+ dstr = datetime.strptime(in_file.read(),
+ DT_FRMT).strftime("%Y-%m-%dT%H")
+ self.assertEqual(dstr, now_time.strftime("%Y-%m-%dT%H"))
+
+ dummy_clock.advance(30)
+ dummy_clock.advance(30)
+ dummy_clock.advance(30)
+ dummy_clock.advance(30)
+ dummy_clock.advance(30)
+ dummy_clock.advance(30)
+ # Here we pretend they ran yesterday so to re-trigger the daily
+ # tasks
+ for t in scheduler_service._scheduled_tasks:
+ with open(os.path.join(self.scheduler_directory,
+ t.identifier), 'w') as out_file:
+ yesterday = (now_time - timedelta(days=1,
+ hours=2)).strftime(DT_FRMT)
+ out_file.write(yesterday)
+ dummy_clock.advance(30)
+
+ # We check that the run method of the deck was called twice
+ self.mock_deck.run.assert_has_calls([
+ mock.call(mock_director), mock.call(mock_director)
+ ])
+ d.callback(None)
+
+ return d
+
+ @mock.patch('ooni.agent.scheduler.resources')
+ @mock.patch('ooni.agent.scheduler.probe_ip')
+ @mock.patch('ooni.agent.scheduler.input_store')
+ @mock.patch('ooni.agent.scheduler.oonireport')
+ def test_disk_quota_cleanup(self, mock_resources, mock_probe_ip,
+ mock_input_store, mock_oonireport):
+
+ mock_probe_ip.geodata['countrycode'] = 'ZZ'
+ mock_probe_ip.lookup.return_value = defer.succeed(None)
+ mock_probe_ip.resolveGeodata.return_value = defer.succeed(None)
+
+ mock_resources.check_for_update.return_value = defer.succeed(None)
+
+ mock_input_store.update.return_value = defer.succeed(None)
+
+ mock_oonireport.upload_all.return_value = defer.succeed(None)
+
+ self.config_mock.basic.measurement_quota = '1M'
+ # We create 10MB of measurements
+ self.create_dummy_measurements(count=10, size=1*1024*1024)
+ measurement_count = len(os.listdir(self.measurements_directory))
+
+ mock_director = mock.MagicMock()
+ d = defer.Deferred()
+ with mock.patch('ooni.agent.scheduler.deck_store', self.deck_store):
+
+ dummy_clock = task.Clock()
+ scheduler_service = SchedulerService(
+ director=mock_director,
+ _reactor=dummy_clock
+ )
+ scheduler_service.startService()
+ dummy_clock.advance(30)
+
+ now_time = datetime.utcnow()
+ DT_FRMT = "%Y-%m-%dT%H:%M:%SZ"
+
+ for t in scheduler_service._scheduled_tasks:
+ with open(os.path.join(self.scheduler_directory,
+ t.identifier)) as in_file:
+ dstr = datetime.strptime(in_file.read(),
+ DT_FRMT).strftime("%Y-%m-%dT%H")
+ self.assertEqual(dstr, now_time.strftime("%Y-%m-%dT%H"))
+
+ # Ensure there are less measurements than there were at the
+ # beginning
+ new_measurement_count = len(os.listdir(self.measurements_directory))
+ self.assertGreater(measurement_count, new_measurement_count)
+
+ d.callback(None)
+
+ return d