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" )
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"])
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" )
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))
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))
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")
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")
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
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)