def test_invalid_configuration_non_existent_file(self): with self.assertRaises(FileNotFoundError): args = argparse.Namespace(cfg='/dev/null') cfg = configure(args) with self.assertRaises(FileNotFoundError): args = argparse.Namespace(cfg='some-non-existent-file') cfg = configure(args)
def test_blastdb_not_found(gke_mock, mocker): """Test that UserReportError is raised when database is not found""" def mocked_check_cluster(cfg): """Mocked check cluster that simulates non-existent cluster status""" return '' mocker.patch('elb.commands.submit.gcp_check_cluster', side_effect=mocked_check_cluster) def mock_safe_exec(cmd): if isinstance(cmd, list): cmd = ' '.join(cmd) if cmd == 'gsutil cat gs://blast-db/latest-dir': return MockedCompletedProcess(stdout='2020-20-20') elif cmd == 'gsutil cat gs://blast-db/2020-20-20/blastdb-manifest.json': return MockedCompletedProcess( stdout='{"nt":{"size":93.36}, "nr":{"size":227.4}}') return MockedCompletedProcess() mocker.patch('elb.util.safe_exec', side_effect=mock_safe_exec) print(INI_NO_BLASTDB) args = Namespace(cfg=INI_NO_BLASTDB) # test that UserReportError is raised with pytest.raises(UserReportError) as err: submit(args, ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT), []) # test error code and message assert err.value.returncode == constants.BLASTDB_ERROR assert 'BLAST database' in err.value.message assert 'not found' in err.value.message
def test_cluster_name_from_environment(env_config): """Test cluster name from environment overrides everything else""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'gcp-defaults.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) assert cfg.cluster.results == env_config['ELB_RESULTS'] assert cfg.cluster.name == env_config['ELB_CLUSTER_NAME']
def test_multiple_query_files(): """Test getting config with multiple query files""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'multiple-query-files.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) expected_query_files = ['query-file-1', 'query-file-2'] assert sorted( cfg.blast.queries_arg.split()) == sorted(expected_query_files)
def test_default_outfmt(self): """ Test that default optional BLAST parameters has -outfmt 11 set """ args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'minimal-cfg-file.ini')) self.cfg = configure(args) cfg = ElasticBlastConfig(self.cfg, task=ElbCommand.SUBMIT) self.assertEqual(cfg.blast.options.strip(), f'-outfmt {ELB_DFLT_OUTFMT}')
def test_aws_defaults(): """Test that default config parameters are set correctly for AWS""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'aws-defaults.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) check_common_defaults(cfg) assert cfg.cloud_provider.cloud == CSP.AWS assert cfg.cluster.pd_size == constants.ELB_DFLT_AWS_PD_SIZE
def test_mem_limit_too_high(): """Test that setting memory limit that exceeds cloud instance memory triggers an error""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'mem-limit-too-high.ini')) with pytest.raises(UserReportError) as err: cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) assert err.value.returncode == INPUT_ERROR m = re.match(r'Memory limit.*exceeds', err.value.message) assert m is not None
def test_instance_too_small_gcp(): """Test that using too small an instance triggers an error""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'instance-too-small-gcp.ini')) with pytest.raises(UserReportError) as err: cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) cfg.validate() assert err.value.returncode == INPUT_ERROR print(err.value.message) assert 'does not have enough memory' in err.value.message
def test_get_gke_credentials_no_cluster_real(): """Test that util.SafeExecError is raised when getting credentials of a non-existent cluster""" data_dir = os.path.join(os.path.dirname(__file__), 'data') args = Namespace(cfg=os.path.join(data_dir, 'test-cfg-file.ini')) cfg = ElasticBlastConfig(config.configure(args), task=ElbCommand.SUBMIT) cfg.cluster.name = 'some-strange-cluster-name' assert cfg.cluster.name not in gcp.get_gke_clusters(cfg) with pytest.raises(SafeExecError): gcp.get_gke_credentials(cfg)
def test_generated_cluster_name(env_config_no_cluster): """Test cluster name generated from results, and value from config file is ignored""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'gcp-defaults.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) assert cfg.cluster.results == TEST_RESULTS_BUCKET user = getpass.getuser() digest = hashlib.md5(TEST_RESULTS_BUCKET.encode()).hexdigest()[0:9] assert cfg.cluster.name == f'elasticblast-{user.lower()}-{digest}'
def test_optional_blast_parameters(self): """ Test that optional BLAST parameters properly read from config file """ args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'optional-cfg-file.ini')) self.cfg = configure(args) cfg = ElasticBlastConfig(self.cfg, task=ElbCommand.SUBMIT) # str.find is not enough here, need to make sure options are properly merged # with whitespace around them. options = cfg.blast.options.strip() self.assertTrue(re.search('(^| )-outfmt 11($| )', options) != None) self.assertTrue( re.search('(^| )-task blastp-fast($| )', options) != None)
def test_label_persistent_disk(safe_exec_mock): """Exercises label_persistent_disk with mock safe_exec and prints out arguments to safe_exec Run pytest -s -v tests/kubernetes to verify correct order of calls""" from argparse import Namespace args = Namespace( cfg=os.path.join(TEST_DATA_DIR, 'initialize_persistent_disk.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) # Replace labels with well-known fake for the purpose of testing command match, # see above in safe_exec_mock cfg.cluster.labels = FAKE_LABELS kubernetes.label_persistent_disk(cfg)
def test_gcp_defaults(): """Test that default config parameters are set correctly for GCP""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'gcp-defaults.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) check_common_defaults(cfg) assert cfg.cloud_provider.cloud == CSP.GCP assert cfg.cluster.pd_size == constants.ELB_DFLT_GCP_PD_SIZE assert cfg.timeouts.blast_k8s == constants.ELB_DFLT_BLAST_K8S_TIMEOUT assert cfg.timeouts.init_pv == constants.ELB_DFLT_INIT_PV_TIMEOUT
def test_optional_blast_parameters_from_command_line(self): """ Test that parameters are read correctly from command line """ args = argparse.Namespace(cfg=os.path.join(TEST_DATA_DIR, 'optional-cfg-file.ini'), blast_opts=['-outfmt', '8']) print(args) self.cfg = configure(args) cfg = ElasticBlastConfig(self.cfg, task=ElbCommand.SUBMIT) self.assertTrue( re.search('(^| )-outfmt 8($| )', cfg.blast.options.strip()) != None ) # NB - options are treated as single entity and command line overwrites them all, not merge, not overwrites selectively self.assertTrue( cfg.blast.options.strip().find('-task blastp-fast') < 0)
def test_initialize_persistent_disk_failed(mocker): def fake_safe_exec_failed_job(cmd): fn = os.path.join(TEST_DATA_DIR, 'job-status-failed.json') return MockedCompletedProcess(stdout=Path(fn).read_text()) mocker.patch('elb.kubernetes.safe_exec', side_effect=fake_safe_exec_failed_job) from argparse import Namespace args = Namespace( cfg=os.path.join(TEST_DATA_DIR, 'initialize_persistent_disk.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) with pytest.raises(RuntimeError): kubernetes.initialize_persistent_disk(cfg, cfg.blast.db) kubernetes.safe_exec.assert_called()
def provide_disk(): """Fixture function that creates GCP disk when setting up a test and deletes it when tearing the test down, returns disk name.""" # test setup name = os.environ['USER'] + '-elastic-blast-test-suite' data_dir = os.path.join(os.path.dirname(__file__), 'data') args = Namespace(cfg=os.path.join(data_dir, 'test-cfg-file.ini')) cfg = ElasticBlastConfig(config.configure(args), task=ElbCommand.SUBMIT) cmd = f'gcloud beta compute disks create {name} --project={cfg.gcp.project} --type=pd-standard --size=10GB --zone={cfg.gcp.zone}' gcp.safe_exec(cmd.split()) yield name, cfg # test teardown if name in gcp.get_disks(cfg): gcp.delete_disk(name, cfg)
def test_initialize_persistent_disk(safe_exec_mock, mocker): """Exercises initialize_persistent_disk with mock safe_exec and prints out arguments to safe_exec Run pytest -s -v tests/kubernetes to verify correct order of calls""" from argparse import Namespace def mocked_upload_file_to_gcs(fname, loc, dryrun): """Mocked upload to GS function""" pass mocker.patch('elb.kubernetes.upload_file_to_gcs', side_effect=mocked_upload_file_to_gcs) args = Namespace( cfg=os.path.join(TEST_DATA_DIR, 'initialize_persistent_disk.ini')) cfg = ElasticBlastConfig(configure(args), task=ElbCommand.SUBMIT) kubernetes.initialize_persistent_disk(cfg, cfg.blast.db)
def provide_cluster(): """Create a GCKE cluster before and delete it after a test""" # setup data_dir = os.path.join(os.path.dirname(__file__), 'data') args = Namespace(cfg=os.path.join(data_dir, 'test-cfg-file.ini')) cfg = ElasticBlastConfig(config.configure(args), task=ElbCommand.SUBMIT) cfg.cluster.name = cfg.cluster.name + f'-{os.environ["USER"]}' cmd = f'gcloud container clusters create {cfg.cluster.name} --num-nodes 1 --machine-type n1-standard-1 --labels elb=test-suite' gcp.safe_exec(cmd.split()) yield cfg # teardown name = cfg.cluster.name if name in gcp.get_gke_clusters(cfg): cmd = f'gcloud container clusters delete {name} -q' gcp.safe_exec(cmd.split())
def test_load_config_from_environment(env_config): """Test config values set from environment""" args = argparse.Namespace() cfg = configure(args) assert cfg[CFG_CLOUD_PROVIDER][CFG_CP_GCP_PROJECT] == env_config[ 'ELB_GCP_PROJECT'] assert cfg[CFG_CLOUD_PROVIDER][CFG_CP_GCP_REGION] == env_config[ 'ELB_GCP_REGION'] assert cfg[CFG_CLOUD_PROVIDER][CFG_CP_GCP_ZONE] == env_config[ 'ELB_GCP_ZONE'] assert cfg[CFG_BLAST][CFG_BLAST_BATCH_LEN] == env_config['ELB_BATCH_LEN'] assert cfg[CFG_CLUSTER][CFG_CLUSTER_NAME] == env_config['ELB_CLUSTER_NAME'] assert cfg[CFG_CLUSTER][CFG_CLUSTER_USE_PREEMPTIBLE] == env_config[ 'ELB_USE_PREEMPTIBLE'] assert cfg[CFG_CLUSTER][CFG_CLUSTER_BID_PERCENTAGE] == env_config[ 'ELB_BID_PERCENTAGE']
def main(): """Local main entry point which sets up arguments, undo stack, and processes exceptions """ try: signal.signal(signal.SIGINT, signal.default_int_handler) clean_up_stack = [] # Check parameters for Unicode letters and reject if codes higher than 255 occur reject_cli_args_with_unicode(sys.argv[1:]) parser = create_arg_parser() args = parser.parse_args() if not args.subcommand: # report missing command line task raise UserReportError(returncode=constants.INPUT_ERROR, message=NO_TASK_MSG) config_logging(args) cfg = configure(args) logging.info(f"ElasticBLAST {args.subcommand} {VERSION}") task = ElbCommand(args.subcommand.lower()) cfg = ElasticBlastConfig(cfg, task=task) logging.debug(pprint.pformat(cfg.asdict())) check_prerequisites(cfg) #TODO: use cfg only when args.wait, args.sync, and args.run_label are replicated in cfg return args.func(args, cfg, clean_up_stack) except (SafeExecError, UserReportError) as e: logging.error(e.message) # SafeExecError return code is the exit code from command line # application ran via subprocess if isinstance(e, SafeExecError): return constants.DEPENDENCY_ERROR return e.returncode except KeyboardInterrupt: return constants.INTERRUPT_ERROR #TODO: process filehelper.TarReadError here finally: messages = clean_up(clean_up_stack) if messages: for msg in messages: logging.error(msg) sys.exit(constants.UNKNOWN_ERROR)
def test_minimal_configuration(self): """Test the auto-configurable parameters""" args = argparse.Namespace( cfg=os.path.join(TEST_DATA_DIR, 'minimal-cfg-file.ini')) self.cfg = configure(args) cfg = ElasticBlastConfig(self.cfg, task=ElbCommand.SUBMIT) self.assertTrue(cfg.blast.db_source) self.assertEqual(cfg.blast.db_source, DBSource.GCP) self.assertTrue(cfg.blast.batch_len) self.assertEqual(cfg.blast.batch_len, 10000) self.assertTrue(cfg.blast.mem_request) self.assertEqual(cfg.blast.mem_request, '0.5G') self.assertTrue(cfg.blast.mem_limit) expected_mem_limit = f'{get_machine_properties(cfg.cluster.machine_type).memory - SYSTEM_MEMORY_RESERVE}G' self.assertEqual(cfg.blast.mem_limit, expected_mem_limit) self.assertTrue(cfg.timeouts.init_pv > 0) self.assertTrue(cfg.timeouts.blast_k8s > 0) ElasticBlastConfig(self.cfg, task=ElbCommand.SUBMIT)