class MainTests(BaseTwistedTestCase):
    """ Basic tests to check main.Main """

    @defer.inlineCallbacks
    def setUp(self):
        """ Sets up a test. """
        yield super(MainTests, self).setUp()
        self.root = self.mktemp('root')
        self.shares = self.mktemp('shares')
        self.data = self.mktemp('data')
        self.partials_dir = self.mktemp('partials_dir')

        self.patch(main_mod, 'SyncdaemonService', FakedExternalInterface)
        # no status listener by default
        self.patch(main_mod.status_listener, "get_listener", lambda *a: None)

        self.handler = MementoHandler()
        self.handler.setLevel(logging.DEBUG)
        self._logger = logging.getLogger('ubuntuone.SyncDaemon')
        self._logger.addHandler(self.handler)
        self.addCleanup(self._logger.removeHandler, self.handler)

    def _get_main_common_params(self):
        """Return the parameters used by the all platforms."""
        return dict(root_dir=self.root,
                    shares_dir=self.shares,
                    data_dir=self.data,
                    partials_dir=self.partials_dir,
                    host='localhost', port=0,
                    dns_srv=False, ssl=False,
                    mark_interval=60,
                    handshake_timeout=2,
                    auth_credentials=FAKED_CREDENTIALS,
                    monitor_class=FakeMonitor)

    def build_main(self, **kwargs):
        """Build and return a Main object.

        Use reasonable defaults for the tests, plus whatever extra kwargs are
        passed in.

        """
        # get the params using the platform code to ensure they are correct
        params = self._get_main_common_params()
        params.update(kwargs)
        m = main_mod.Main(**params)
        self.addCleanup(m.shutdown)
        m.local_rescan = lambda *_: m.event_q.push('SYS_LOCAL_RESCAN_DONE')
        return m

    def test_main_initialization(self):
        """test that creating a Main instance works as expected."""
        main = self.build_main()
        self.assertIsInstance(main, main_mod.Main)

    def test_main_start(self):
        """Test that Main.start works."""
        main = self.build_main()
        main.start()

    def test_main_restarts_on_critical_error(self):
        """Test that Main restarts when syncdaemon gets into UNKNOWN_ERROR."""
        self.restarted = False
        main = self.build_main()
        main.restart = lambda: setattr(self, 'restarted', True)
        main.start()
        main.event_q.push('SYS_UNKNOWN_ERROR')
        self.assertTrue(self.restarted)

    @defer.inlineCallbacks
    def test_shutdown_pushes_sys_quit(self):
        """When shutting down, the SYS_QUIT event is pushed."""
        params = self._get_main_common_params()
        main = main_mod.Main(**params)
        events = []
        self.patch(main.event_q, 'push',
                   lambda *a, **kw: events.append((a, kw)))

        yield main.shutdown()
        expected = [(('SYS_USER_DISCONNECT',), {}), (('SYS_QUIT',), {})]
        self.assertEqual(expected, events)

    def test_handshake_timeout(self):
        """Check connecting times out."""
        d0 = defer.Deferred()

        class Handler:
            """Trivial event handler."""
            def handle_SYS_HANDSHAKE_TIMEOUT(self):
                """Pass the test when we get this event."""
                reactor.callLater(0, d0.callback, None)

        main = self.build_main(handshake_timeout=0)

        def fake_connect(*a):
            """Only connect when States told so."""
            main.event_q.push('SYS_CONNECTION_MADE')
            return defer.Deferred()
        main.action_q.connect = fake_connect

        # fake the following to not be executed
        main.get_root = lambda *_: defer.Deferred()
        main.action_q.check_version = lambda *_: defer.Deferred()

        main.event_q.subscribe(Handler())
        main.start()
        main.event_q.push('SYS_NET_CONNECTED')
        main.event_q.push('SYS_USER_CONNECT', access_token='')
        return d0

    def test_create_dirs_already_exists_dirs(self):
        """test that creating a Main instance works as expected."""
        link = os.path.join(self.root, 'Shared With Me')
        self.assertFalse(is_link(link))
        self.assertTrue(path_exists(self.shares))
        self.assertTrue(path_exists(self.root))
        main = self.build_main()
        # check that the shares link is actually a link
        self.assertTrue(is_link(main.shares_dir_link))
        self.assertEquals(link, main.shares_dir_link)

    def test_create_dirs_already_exists_symlink_too(self):
        """test that creating a Main instance works as expected."""
        link = os.path.join(self.root, 'Shared With Me')
        make_link(self.shares, link)
        self.assertTrue(is_link(link))
        self.assertTrue(path_exists(self.shares))
        self.assertTrue(path_exists(self.root))
        main = self.build_main()
        # check that the shares link is actually a link
        self.assertTrue(is_link(main.shares_dir_link))

    def test_create_dirs_already_exists_but_not_symlink(self):
        """test that creating a Main instance works as expected."""
        link = os.path.join(self.root, 'Shared With Me')
        make_dir(link, recursive=True)
        self.assertTrue(path_exists(link))
        self.assertFalse(is_link(link))
        self.assertTrue(path_exists(self.shares))
        self.assertTrue(path_exists(self.root))
        main = self.build_main()
        # check that the shares link is actually a link
        self.assertEquals(main.shares_dir_link, link)
        self.assertFalse(is_link(main.shares_dir_link))

    def test_create_dirs_none_exists(self):
        """test that creating a Main instance works as expected."""
        # remove the existing dirs
        remove_dir(self.root)
        remove_dir(self.shares)
        main = self.build_main()
        # check that the shares link is actually a link
        self.assertTrue(is_link(main.shares_dir_link))
        self.assertTrue(path_exists(self.shares))
        self.assertTrue(path_exists(self.root))

    def test_connect_if_autoconnect_is_enabled(self):
        """If autoconnect option is enabled, connect the syncdaemon."""
        user_config = main_mod.config.get_user_config()
        orig = user_config.get_autoconnect()
        user_config.set_autoconnect(True)
        self.addCleanup(user_config.set_autoconnect, orig)

        main = self.build_main()
        expected = [('connect', (), {'autoconnecting': True})]
        self.assertEqual(main.external._called, expected)

    def test_dont_connect_if_autoconnect_is_disabled(self):
        """If autoconnect option is disabled, do not connect the syncdaemon."""
        user_config = main_mod.config.get_user_config()
        orig = user_config.get_autoconnect()
        user_config.set_autoconnect(False)
        self.addCleanup(user_config.set_autoconnect, orig)

        main = self.build_main()
        self.assertEqual(main.external._called, [])

    def _get_listeners(self, main):
        """Return the subscribed objects."""
        s = set()
        for listener in main.event_q.listener_map.values():
            for x in listener:
                s.add(x)
        return s

    def test_status_listener_is_installed(self):
        """The status listener is installed if needed."""
        self.patch(main_mod.status_listener,
                   "get_listener", lambda *a: FakeListener())
        main = self.build_main()
        self.assertIn(main.status_listener, self._get_listeners(main))

    def test_status_listener_not_installed_when_disabled(self):
        """The status listener is not started if it's not available."""
        main = self.build_main()
        self.assertNotIn(main.status_listener, self._get_listeners(main))

    def test_get_homedir(self):
        """The get_homedir returns the root dir."""
        self.patch(main_mod, "user_home", self.home_dir)
        expected = expand_user('~')
        main = self.build_main()
        self.assertEqual(main.get_homedir(), expected)

    def test_get_rootdir(self):
        """The get_rootdir returns the root dir."""
        expected = expand_user(os.path.join('~', 'Ubuntu Test One'))
        main = self.build_main(root_dir=expected)
        self.assertEqual(main.get_rootdir(), expected)

    def test_get_sharesdir(self):
        """The get_sharesdir returns the shares dir."""
        expected = expand_user(os.path.join('~', 'Share it to Me'))
        main = self.build_main(shares_dir=expected)
        self.assertEqual(main.get_sharesdir(), expected)

    def test_get_sharesdirlink(self):
        """The get_sharesdirlink returns the shares dir link."""
        expected = 'Share it to Me'
        main = self.build_main(shares_symlink_name=expected)
        self.assertEqual(main.get_sharesdir_link(),
                         os.path.join(main.get_rootdir(), expected))

    def test_version_is_logged(self):
        """Test that the client version is logged."""
        self.build_main()
        self.assertTrue(self.handler.check_info("client version", VERSION))

    def test_mark(self):
        """Check the MARK logs ok."""
        main = self.build_main()
        main.log_mark()
        shouldlog = ('MARK', "State: 'INIT'", 'queues IDLE', 'connection',
                     'queue: 0', 'offloaded: 0', 'hash: 0')
        self.assertTrue(self.handler.check(NOTE, *shouldlog))
class ReactorInspectorTestCase(TwistedTestCase):
    """Test the ReactorInspector class."""

    def setUp(self):
        """Set up."""
        class Helper(object):
            """Fake object with a controllable call."""
            def __init__(self):
                self.call_count = 1
                self.calls = []
                self.ri = None

            def call(self, func):
                """Call function when counter is 0, then stop running."""
                self.call_count -= 1
                self.calls.append(func)
                if self.call_count == 0:
                    for f in self.calls:
                        f()
                if self.call_count <= 0:
                    self.ri.stop()

        class FakeMetrics(object):
            """Fake Metrics object that records calls."""
            def __init__(self):
                """Initialize calls."""
                self.calls = []

            def meter(self, name, count):
                """Record call to meter()."""
                self.calls.append(("meter", name, count))

            def gauge(self, name, val):
                """Record call to gauge()."""
                self.calls.append(("gauge", name, val))

        logger = logging.getLogger("storage.server")
        logger.propagate = False
        logger.setLevel(TRACE)
        self.handler = MementoHandler()
        self.handler.setLevel(TRACE)
        logger.addHandler(self.handler)
        self.addCleanup(logger.removeHandler, self.handler)
        self.helper = Helper()
        self.fake_metrics = FakeMetrics()
        MetricsConnector.register_metrics("reactor_inspector",
                                          instance=self.fake_metrics)
        self.addCleanup(MetricsConnector.unregister_metrics)
        self.ri = ReactorInspector(logger, self.helper.call, loop_time=.1)
        self.helper.ri = self.ri

    def run_ri(self, call_count=None, join=True):
        """Set the call count and then run the ReactorInspector."""
        if call_count is not None:
            self.helper.call_count = call_count
        self.start_ts = time.time()
        self.ri.start()
        # Reactor will stop after call_count calls, thanks to helper
        if join:
            self.ri.join()

    def test_stop(self):
        """It stops."""
        self.run_ri(1000, join=False)
        assert self.ri.is_alive()
        self.ri.stop()
        self.ri.join()
        self.assertFalse(self.ri.is_alive())

    @defer.inlineCallbacks
    def test_dump_frames(self):
        """Test how frames are dumped.

        Rules:
        - own frame must not be logged
        - must log all other threads
        - main reactor thread must have special title
        """
        # other thread, whose frame must be logged
        waitingd = defer.Deferred()

        def waiting_function():
            """Function with funny name to be checked later."""
            reactor.callFromThread(waitingd.callback, True)
            # wait have a default value
            event.wait()

        event = threading.Event()
        threading.Thread(target=waiting_function).start()
        # Make sure the thread has entered the waiting_function
        yield waitingd

        # Set reactor_thread since we're not starting the ReactorInspector
        # thread here.
        self.ri.reactor_thread = threading.currentThread().ident

        # dump frames in other thread, also
        def dumping_function():
            """Function with funny name to be checked later."""
            time.sleep(.1)
            self.ri.dump_frames()
            reactor.callFromThread(d.callback, True)

        d = defer.Deferred()
        threading.Thread(target=dumping_function).start()
        yield d
        event.set()

        # check
        self.assertFalse(self.handler.check_debug("dumping_function"))
        self.assertTrue(self.handler.check_debug("Dumping Python frame",
                                                 "waiting_function"))
        self.assertTrue(self.handler.check_debug("Dumping Python frame",
                                                 "reactor main thread"))

    def test_reactor_ok(self):
        """Reactor working fast."""
        self.run_ri()
        ok_line = self.handler.check(TRACE, "ReactorInspector: ok")
        self.assertTrue(ok_line)
        self.assertTrue(ok_line.args[-1] >= 0)  # Should be near zero delay
        # Check the metrics
        expected_metric = ("gauge", "delay", ok_line.args[-1])
        self.assertEqual([expected_metric], self.fake_metrics.calls)
        self.assertTrue(self.ri.last_responsive_ts >= self.start_ts)

    @defer.inlineCallbacks
    def test_reactor_blocked(self):
        """Reactor not working fast."""
        dump_frames_called = defer.Deferred()
        self.ri.dump_frames = lambda: dump_frames_called.callback(True)
        self.run_ri(0)
        yield dump_frames_called
        log_line = self.handler.check(logging.CRITICAL, "ReactorInspector",
                                      "detected unresponsive")
        self.assertTrue(log_line)
        self.assertTrue(log_line.args[-1] >= .1)  # waited for entire loop time
        # Check the metrics
        expected_metric = ("gauge", "delay", log_line.args[-1])
        self.assertEqual([expected_metric], self.fake_metrics.calls)

        self.assertTrue(self.ri.last_responsive_ts < self.start_ts)

    def test_reactor_back_alive(self):
        """Reactor resurrects after some loops."""
        self.run_ri(3)
        late_line = self.handler.check_warning("ReactorInspector: late",
                                               "got: 0")
        self.assertTrue(late_line)
        self.assertTrue(late_line.args[-1] >= .2)  # At least 2 cycles of delay
        # Check the metrics
        expected_metric = ("gauge", "delay", late_line.args[-1])
        self.assertEqual(expected_metric, self.fake_metrics.calls[-1])

        self.assertTrue(self.ri.queue.empty())
        # A late reactor is not considered responsive (until a successful loop)
        self.assertTrue(self.ri.last_responsive_ts < self.start_ts)