Esempio n. 1
0
    def test_spawn_uneven_locusts(self):
        """
        Tests that we can accurately spawn a certain number of locusts, even if it's not an 
        even number of the connected slaves
        """
        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.Server",
                        mocked_rpc_server()) as server:
            master = MasterLocustRunner(MyTestLocust, self.options)
            for i in range(5):
                server.mocked_send(
                    Message("client_ready", None, "fake_client%i" % i))

            master.start_hatching(7, 7)
            self.assertEqual(5, len(server.outbox))

            num_clients = 0
            for _, msg in server.outbox:
                num_clients += Message.unserialize(msg).data["num_clients"]

            self.assertEqual(
                7, num_clients,
                "Total number of locusts that would have been spawned is not 7"
            )
Esempio n. 2
0
 def test_rebalance_locust_users_on_slave_connect(self):
     class MyTestLocust(Locust):
         pass
     
     with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server:
         master = MasterLocustRunner(MyTestLocust, self.options)
         server.mocked_send(Message("client_ready", None, "zeh_fake_client1"))
         self.assertEqual(1, len(master.clients))
         self.assertTrue("zeh_fake_client1" in master.clients, "Could not find fake client in master instance's clients dict")
         
         master.start_hatching(100, 20)
         self.assertEqual(1, len(server.outbox))
         client_id, msg = server.outbox.pop()
         self.assertEqual(100, msg.data["num_clients"])
         self.assertEqual(20, msg.data["hatch_rate"])
         
         # let another slave connect
         server.mocked_send(Message("client_ready", None, "zeh_fake_client2"))
         self.assertEqual(2, len(master.clients))
         self.assertEqual(2, len(server.outbox))
         client_id, msg = server.outbox.pop()
         self.assertEqual(50, msg.data["num_clients"])
         self.assertEqual(10, msg.data["hatch_rate"])
         client_id, msg = server.outbox.pop()
         self.assertEqual(50, msg.data["num_clients"])
         self.assertEqual(10, msg.data["hatch_rate"])
Esempio n. 3
0
    def test_spawn_fewer_locusts_than_slaves(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.Server",
                        mocked_rpc_server()) as server:
            master = MasterLocustRunner(MyTestLocust, self.options)
            for i in range(5):
                server.mocked_send(
                    Message("client_ready", None, "fake_client%i" % i))
                sleep(0)

            master.start_hatching(2, 2)
            self.assertEqual(5, len(server.outbox))

            num_clients = 0
            for msg in server.outbox:
                num_clients += Message.unserialize(msg).data["num_clients"]

            self.assertEqual(
                2, num_clients,
                "Total number of locusts that would have been spawned is not 2"
            )
Esempio n. 4
0
    def test_sends_hatch_data_to_ready_running_hatching_drones(self):
        '''Sends hatch job to running, ready, or hatching drones'''
        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server:
            master = MasterLocustRunner(MyTestLocust, self.options)
            master.clients[1] = DroneNode(1)
            master.clients[2] = DroneNode(2)
            master.clients[3] = DroneNode(3)
            master.clients[1].state = STATE_INIT
            master.clients[2].state = STATE_HATCHING
            master.clients[3].state = STATE_RUNNING
            master.start_hatching(5,5)

            self.assertEqual(3, len(server.outbox))
Esempio n. 5
0
    def test_sends_hatch_data_to_ready_running_hatching_slaves(self):
        '''Sends hatch job to running, ready, or hatching slaves'''
        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server:
            master = MasterLocustRunner(MyTestLocust, self.options)
            master.clients[1] = SlaveNode(1)
            master.clients[2] = SlaveNode(2)
            master.clients[3] = SlaveNode(3)
            master.clients[1].state = STATE_INIT
            master.clients[2].state = STATE_HATCHING
            master.clients[3].state = STATE_RUNNING
            master.start_hatching(5,5)

            self.assertEqual(3, len(server.outbox))
Esempio n. 6
0
 def test_spawn_fewer_locusts_than_slaves(self):
     class MyTestLocust(Locust):
         pass
     
     with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server:
         master = MasterLocustRunner(MyTestLocust, self.options)
         for i in range(5):
             server.mocked_send(Message("client_ready", None, "fake_client%i" % i))
         
         master.start_hatching(2, 2)
         self.assertEqual(5, len(server.outbox))
         
         num_clients = 0
         for _, msg in server.outbox:
             num_clients += Message.unserialize(msg).data["num_clients"]
         
         self.assertEqual(2, num_clients, "Total number of locusts that would have been spawned is not 2")
Esempio n. 7
0
 def test_spawn_uneven_locusts(self):
     """
     Tests that we can accurately spawn a certain number of locusts, even if it's not an 
     even number of the connected slaves
     """
     class MyTestLocust(Locust):
         pass
     
     with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server:
         master = MasterLocustRunner(MyTestLocust, self.options)
         for i in range(5):
             server.mocked_send(Message("client_ready", None, "fake_client%i" % i))
         
         master.start_hatching(7, 7)
         self.assertEqual(5, len(server.outbox))
         
         num_clients = 0
         for _, msg in server.outbox:
             num_clients += Message.unserialize(msg).data["num_clients"]
         
         self.assertEqual(7, num_clients, "Total number of locusts that would have been spawned is not 7")
Esempio n. 8
0
class Plugin(AbstractPlugin, GeneratorPlugin):

    """ Locust tank plugin """
    SECTION = 'locust'


    def __init__(self, core, cfg, cfg_updater):
        AbstractPlugin.__init__(self, core, cfg, cfg_updater)
        self.core = core
        self._locustrunner = None
        self._locustclasses = None
        self._options = None
        self._user_count = 0
        self._state = ''
        self._locuststats = ''
        self._locustslaves = None
        self.stats_reader = None
        self.reader = None
        self.host = None
        self.web_host = ''
        self.port = 8089
        self.locustfile = 'locustfile'
        self.master = False
        self.slave = False
        self.master_host = "127.0.0.1"
        self.master_port = 5557
        self.master_bind_host = "*"
        self.master_bind_port = 5557
        self.expect_slaves = 0
        self.no_web = True
        self.num_clients = int(1)
        self.hatch_rate = float(1)
        self.num_requests = None
        self.run_time = None
        self.loglevel = 'INFO'
        self.logfile = None
        self.csvfilebase = None
        self.csvappend = True
        self.print_stats = True
        self.only_summary = True
        self.list_commands = False
        self.show_task_ratio = False
        self.show_task_ratio_json = False
        self.show_version = True
        self.locustlog_level = 'INFO'
        self.cfg = cfg

        # setup logging
        ll.setup_logging(self.loglevel, self.logfile)

    @property
    def locustlog_file(self):
        logger.debug("######## DEBUG: self.core.artifacts_dir = {}".format(self.core.artifacts_dir))
        return "{}/locust.log".format(self.core.artifacts_dir)


    def get_available_options(self):
        return [
            "host", "port", "locustfile",
            "num_clients", "hatch_rate", "run_time", #"num_requests",
            "logfile", "loglevel", "csvfilebase",
            "master", "master_bind_host", "master_bind_port", "expect_slaves",
            "master_host", "master_port"
        ]


    def _get_variables(self):
        res = {}
        for option in self.core.config.get_options(self.SECTION):
            if option[0] not in self.get_available_options():
                res[option[0]] = option[1]
        logger.debug("Variables: %s", res)
        return res


    def get_reader(self):
        if self.reader is None:
            self.reader = LocustReader(self, self.locustlog_file)
        return self.reader

    def get_stats_reader(self):
        if self.stats_reader is None:
            self.stats_reader = self.reader.stats_reader
            logger.debug("######## DEBUG: plugin.reader.stats_reader.source = %s" % self.stats_reader.source)
            return self.stats_reader

    def configure(self):
        self.host = self.get_option("host")
        self.port = self.get_option("port")
        self.locustfile = self.get_option("locustfile")
        self.num_clients = int(self.get_option ("num_clients"))
        self.hatch_rate = float(self.get_option("hatch_rate"))
        self.run_time = self.get_option("run_time")
        self.logfile = self.get_option("logfile")
        self.loglevel = self.get_option("loglevel")
        self.csvfilebase = self.get_option("csvfilebase")
        self.locustlog_level = self.get_option("locustlog_level")
        self.show_version = True
        self.master = self.get_option("master")
        self.master_bind_host = self.get_option("master_bind_host")
        self.master_bind_port = self.get_option("master_bind_port")
        self.expect_slaves = self.get_option("expect_slaves")
        self.master_host = self.get_option("master_host")
        self.master_port = self.get_option("master_port")


        if self.locustlog_file:
            logger.debug("######## DEBUG: configuring Locust resplog")
            ll.setup_resplogging(self.locustlog_level, self.locustlog_file)

    def get_options(self):
        options = {optname : self.__getattribute__(optname) for optname in self.get_available_options()}
        logger.debug("##### Locust plugin: get_options() : options = {}".format(options))
        return options

    def prepare_test(self):
        logger = logging.getLogger(__name__)


        try:
            logger.debug("######## DEBUG: looking for a console object")
            ### DEBUG: enable/disable Console
            console = self.core.get_plugin_of_type(ConsolePlugin)
        except Exception as ex:
            logger.debug("######## DEBUG: Console not found: %s", ex)
            console = None

        if console:
            logger.debug("######## DEBUG: console found")
            widget = LocustInfoWidget(self)
            console.add_info_widget(widget)
            logger.debug("######## DEBUG: locust widget added to console")


        try:

            locustfile = lm.find_locustfile(self.locustfile)
            if not locustfile:
                logger.error("##### Locust plugin: Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.")
                sys.exit(1)

            if locustfile == "locust.py":
                logger.error("##### Locust plugin: The locustfile must not be named `locust.py`. Please rename the file and try again.")
                sys.exit(1)

            docstring, locusts = lm.load_locustfile(locustfile)

            logger.info("##### Locust plugin: locustfile = {}".format(locustfile))

            if not locusts:
                logger.error("##### Locust plugin: No Locust class found!")
                sys.exit(1)
            else:
                logger.info("##### Locust plugin: Locust classes found in {} : {}".format(locustfile, locusts))

            self._locustclasses = list(locusts.values())
            options = Opts(**self.get_options())
            self._options = options
            logger.debug("##### Locust plugin: main() : options = {}".format(options))


        except Exception as e:
            logger.error("##### Locust plugin: prepare_test() CRITICAL ERROR : %s", e)
            sys.exit(1)


    def is_any_slave_up(self):
        if self.master and self._locustslaves is not None:
            poll_slaves = [s.poll() for s in self._locustslaves]
            res = any([False if x is not None else True for x in poll_slaves])
            logger.debug("######## DEBUG: is_any_slave_up/any(res) = {}".format(res))
            return res
        elif self.master:
            logger.error("##### Locust plugin: no slave alive to poll")
            return False
        else:
            return False


    def start_test(self):

        # install SIGTERM handler
        def sig_term_handler():
            logger.info("##### Locust plugin: Got SIGTERM signal")
            self.shutdown(0)
            gevent.signal(signal.SIGTERM, sig_term_handler)

        def spawn_local_slaves(count):
            """
            Spawn *local* locust slaves : data aggregation will NOT work with *remote* slaves
            """
            try:
                args = ['locust']
                args.append('--locustfile={}'.format(str(self.locustfile)))
                args.append('--slave')
                args.append('--master-host={}'.format(self.master_host))
                args.append('--master-port={}'.format(self.master_port))
                args.append('--resplogfile={}'.format(self.locustlog_file))
                logger.info("##### Locust plugin: slave args = {}".format(args))

                # Spawning the slaves in shell processes (security warning with the use of 'shell=True')
                self._locustslaves = [subprocess.Popen(' '.join(args), shell=True, stdin=None,
                            stdout=open('{}/locust-slave-{}.log'.format(self.core.artifacts_dir, i), 'w'),
                            stderr=subprocess.STDOUT) for i in range(count)]
                #slaves = [SlaveLocustRunner(self._locustclasses, self._options) for _ in range(count)] # <-- WRONG: This will spawn slave running on the same CPU core as master
                time.sleep(1)

                logger.info("##### Locust plugin: Started {} new locust slave(s)".format(len(self._locustslaves)))
                logger.info("##### Locust plugin: locust slave(s) PID = {}".format(self._locustslaves))
            except socket.error as e:
                logger.error("##### Locust plugin: Failed to connect to the Locust master: %s", e)
                sys.exit(-1)
            except Exception as e:
                logger.error("##### Locust plugin: Failed to spawn locust slaves: %s", e)
                sys.exit(-1)

        try:
            logger.info("##### Locust plugin: Starting Locust %s" % version)

            # run the locust 

            ### FIXME
            #if self.csvfilebase:
            #    gevent.spawn(stats_writer, self.csvfilebase)
            ### /FIXME

            if self.run_time:
                if not self.no_web:
                    logger.error("##### Locust plugin: The --run-time argument can only be used together with --no-web")
                    sys.exit(1)
                try:
                    self.run_time = parse_timespan(self.run_time)
                except ValueError:
                    logger.error("##### Locust plugin: Valid --time-limit formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.")
                    sys.exit(1)
                def spawn_run_time_limit_greenlet():
                    logger.info("##### Locust plugin: Run time limit set to %s seconds" % self.run_time)
                    def timelimit_stop():
                        logger.info("##### Locust plugin: Time limit reached. Stopping Locust.")
                        self._locustrunner.quit()
                        logger.debug("######## DEBUG: timelimit_stop()/self._locustrunner.quit() passed")
                    def on_greenlet_completion():
                        logger.debug("######## DEBUG: Locust plugin: on_greenlet_completion()")

                    #gevent.spawn_later(self.run_time, timelimit_stop)
                    gl = gevent.spawn_later(self.run_time, timelimit_stop)
                    # linking timelimit greenlet to main greenlet and get a feedback of its execution
                    #gl.link(on_greenlet_completion)



            # locust runner : web monitor
            if not self.no_web and not self.slave and self._locustrunner is None:
                # spawn web greenlet
                logger.info("##### Locust plugin: Starting web monitor at %s:%s" % (self.web_host or "*", self.port))
                main_greenlet = gevent.spawn(web.start, self._locustclasses, self._options)


            # locust runner : standalone
            if not self.master and not self.slave and self._locustrunner is None:
                logger.info("##### Locust plugin: LocalLocustRunner about to be launched")
                self._locustrunner = LocalLocustRunner(self._locustclasses, self._options)
                # spawn client spawning/hatching greenlet
                if self.no_web:
                    logger.info("##### Locust plugin: LocalLocustRunner.start_hatching()")
                    self._locustrunner.start_hatching(wait=True)
                    main_greenlet = self._locustrunner.greenlet
                if self.run_time:
                    logger.info("##### Locust plugin: spawn_run_time_limit_greenlet()")
                    spawn_run_time_limit_greenlet()
                    logger.info("##### Locust plugin: spawn_run_time_limit_greenlet() passed")

            # locust runner : master/slave mode (master here)
            elif self.master and self._locustrunner is None:
                self._locustrunner = MasterLocustRunner(self._locustclasses, self._options)
                logger.info("##### Locust plugin: MasterLocustRunner started")
                time.sleep(1)
                if self.no_web:
                    gevent.spawn(spawn_local_slaves(self.expect_slaves))
                    while len(self._locustrunner.clients.ready) < self.expect_slaves:
                        logger.info("##### Locust plugin: Waiting for slaves to be ready, %s of %s connected",
                                     len(self._locustrunner.clients.ready), self.expect_slaves)
                        time.sleep(1)
                    self._locustrunner.start_hatching(self.num_clients, self.hatch_rate)
                    logger.debug("######## DEBUG: MasterLocustRunner/start_hatching()")
                    main_greenlet = self._locustrunner.greenlet
                if self.run_time:
                    try:
                        spawn_run_time_limit_greenlet()
                    except Exception as e:
                        logger.error("##### Locust plugin: exception raised in spawn_run_time_limit_greenlet() = {}".format(e))

            # locust runner : master/slave mode (slave here)
            #elif self.slave and self._locustrunner is None:
            #    if self.run_time:
            #        logger.error("##### Locust plugin: --run-time should be specified on the master node, and not on slave nodes")
            #        sys.exit(1)
            #    try:
            #        self._locustrunner = SlaveLocustRunner(self._locustclasses, self._options)
            #        main_greenlet = self._locustrunner.greenlet
            #    except socket.error as e:
            #        logger.error("##### Locust plugin: Failed to connect to the Locust master: %s", e)
            #        sys.exit(-1)
            return self._locustrunner

            self._locustrunner.greenlet.join()
            code = 0
            if len(self._locustrunner.errors):
                code = 1
                self.shutdown(code=code)
        except KeyboardInterrupt as e:
            self.shutdown(0)


    def shutdown(self, code=0):
        """
        Shut down locust by firing quitting event, printing stats and exiting
        """


        logger.debug("######## DEBUG: shutdown()/_locustrunner = {}".format(self._locustrunner))
        logger.info("##### Locust plugin: Cleaning up runner...")
        if self._locustrunner is not None and self.is_any_slave_up():
            #if self.csvfilebase:
            #    write_stat_csvs(self.csvfilebase)
            retcode = self._locustrunner.quit()
            logger.debug("######## DEBUG: shutdown()/_locustrunner.quit() passed # retcode = {}".format(retcode))
        logger.info("##### Locust plugin: Running teardowns...")

        while not self.reader.is_stat_queue_empty():
            logger.info("##### Locust plugin: {} items remaining is stats queue".format(self.reader.stat_queue.qsize()))
            time.sleep(1)

        ### FIXME : possibly causing a greenlet looping infinitely
        #events.quitting.fire(reverse=True)
        print_stats(self._locustrunner.request_stats)
        print_percentile_stats(self._locustrunner.request_stats)
        print_error_report()
        self.reader.close()
        logger.info("##### Locust plugin: Shutting down (exit code %s), bye." % code)

    def is_test_finished(self):
        """
        Fetch locustrunner stats: min/max/median/avg response time, current RPS, fail ratio
        """
        if self._locustrunner:
            self._locuststats = self._locustrunner.stats.total

        """
        Fetch locustrunner status: 'ready', 'hatching', 'running', 'stopped' and returns status code
        """
        logger.debug("######## DEBUG: is_test_finished()? -> Fetching locust status")
        logger.debug("######## DEBUG: is_test_finished() -> self._locustrunner.state = {}".format(self._locustrunner.state))
        logger.debug("######## DEBUG: is_test_finished() -> is_any_slave_up() = {}".format(self.is_any_slave_up()))
        self._state = self._locustrunner.state
        if self._locustrunner.state == 'stopped' or self.master and not self.is_any_slave_up():
            self._user_count = 0
            return 0
        else:
            self._user_count = self._locustrunner.user_count
            return -1

    def end_test(self, retcode):
        if self.is_test_finished() < 0:
            logger.info("##### Locust plugin: Terminating Locust")
            self.shutdown(retcode)
        else:
            logger.info("##### Locust plugin: Locust has been terminated already")
            self.shutdown(retcode)
        return retcode
Esempio n. 9
0
class TestMasterRunner(LocustTestCase):
    def setUp(self):
        global_stats.reset_all()
        self._slave_report_event_handlers = [
            h for h in events.node_report._handlers
        ]

    def tearDown(self):
        events.node_report._handlers = self._slave_report_event_handlers
        self.master.quit()

    def test_slave_connect(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "zeh_fake_client1"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                sleep(0)
                self.assertEqual(1, self.master.slave_count)
                self.assertTrue(
                    "zeh_fake_client1" in self.master.slaves,
                    "Could not find fake client in master instance's clients dict"
                )
                server.mocked_send(
                    "all", Message("slave_ready", None, "zeh_fake_client2"))
                server.mocked_send(
                    "all", Message("slave_ready", None, "zeh_fake_client3"))
                server.mocked_send(
                    "all", Message("slave_ready", None, "zeh_fake_client4"))
                sleep(0)
                self.assertEqual(4, self.master.slave_count)

                server.mocked_send("all",
                                   Message("quit", None, "zeh_fake_client3"))
                sleep(0)
                self.assertEqual(3, self.master.slave_count)

    def test_automatic_options_propagate(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "zeh_fake_client1"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                sleep(0)
                self.assertEqual(1, self.master.slave_count)
                self.assertEqual(1, len(server.outbox_direct))
                msg = Message.unserialize(server.outbox_direct[0][1]).data
                self.assertEqual(self.master.options._config, msg)

    def test_on_demand_options_propagate(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "zeh_fake_client1"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                sleep(0)
                self.assertEqual(1, self.master.slave_count)
                server.outbox_all = []
                config_upd = {
                    'host': 'custom_host.com',
                    'master_host': 'new_master_host.com'
                }
                self.master.propagate_config(config_upd)
                self.assertEqual(1, len(server.outbox_all))

                msg = Message.unserialize(server.outbox_all[0]).data
                self.assertEqual(msg['host'], 'custom_host.com')
                self.assertEqual(msg['master_host'], '127.0.0.1')

    def test_slave_stats_report_median(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send('all',
                                   Message("slave_ready", None, "fake_client"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                sleep(0)

                self.master.stats.get("Task", "/", "GET").log(100, 23455)
                self.master.stats.get("Task", "/", "GET").log(800, 23455)
                self.master.stats.get("Task", "/", "GET").log(700, 23455)

                data = {"user_count": 1, "worker_count": 1}
                events.report_to_master.fire(node_id="fake_client", data=data)
                self.master.stats.clear_all()

                server.mocked_send("all", Message("stats", data,
                                                  "fake_client"))
                sleep(0)
                s = self.master.stats.get("Task", "/", "GET")
                self.assertEqual(700, s.median_response_time)

    def test_spawn_uneven_locusts(self):
        """
        Tests that we can accurately spawn a certain number of locusts, even if it's not an
        even number of the connected slaves
        """
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "fake_client0"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                for i in range(1, 5):
                    server.mocked_send(
                        "all", Message("slave_ready", None,
                                       "fake_client%i" % i))
                    sleep(0)

                server.outbox_direct = []
                self.master.start_hatching(7, 7)
                self.assertEqual(5, len(server.outbox_direct))

                num_clients = 0
                for msg in server.outbox_direct:
                    num_clients += Message.unserialize(
                        msg[1]).data["num_clients"]
                self.assertEqual(
                    7, num_clients,
                    "Total number of locusts that would have been spawned is not 7"
                )

    def test_spawn_fewer_locusts_than_slaves(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "fake_client0"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())
                for i in range(1, 5):
                    server.mocked_send(
                        'all', Message("slave_ready", None,
                                       "fake_client%i" % i))
                    sleep(0)

                server.outbox_direct = []
                self.master.start_hatching(2, 2)
                self.assertEqual(5, len(server.outbox_direct))

                num_clients = 0
                for msg in server.outbox_direct:
                    num_clients += Message.unserialize(
                        msg[1]).data["num_clients"]

                self.assertEqual(
                    2, num_clients,
                    "Total number of locusts that would have been spawned is not 2"
                )

    def test_heartbeat(self):
        import mock

        class MyTestLocust(Locust):
            pass

        with mock.patch("locust.rpc.rpc.MasterServer",
                        mocked_rpc_server()) as server:
            with mock.patch("locust.runners.master.Process", mocked_process()):
                server.mocked_send(
                    'all', Message("slave_ready", None, "fake_client0"))
                self.master = MasterLocustRunner(MyTestLocust,
                                                 config.locust_config())

                sleep(0)
                self.assertEqual(1, len(server.outbox_all))
                self.assertEqual(
                    'ping',
                    Message.unserialize(server.outbox_all[0]).type)
                server.mocked_send('all', Message("pong", None,
                                                  "fake_client0"))
                sleep(runners.master.HEARTBEAT_INTERVAL)
                self.assertEqual(1, self.master.slave_count)
                sleep(runners.master.HEARTBEAT_INTERVAL)
                self.assertEqual(0, self.master.slave_count)