From 5499f93421730a493e90903be62de681e1102a45 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 14 Aug 2024 08:04:23 -0700 Subject: [PATCH 01/10] test: Remove unnecessary assertion warnings and update exit codes in tests (#632) --- .../domains/test_domain_records.py | 53 +++++++++++++------ .../integration/domains/test_domains_tags.py | 7 ++- .../domains/test_master_domains.py | 7 ++- tests/integration/firewalls/test_firewalls.py | 10 ++-- tests/integration/kernels/test_kernels.py | 26 ++++----- tests/integration/linodes/test_linodes.py | 4 +- .../integration/linodes/test_power_status.py | 2 +- tests/integration/linodes/test_rebuild.py | 4 +- tests/integration/linodes/test_resize.py | 17 ++++-- .../nodebalancers/test_node_balancers.py | 10 ++-- .../stackscripts/test_stackscripts.py | 7 ++- tests/integration/tags/test_tags.py | 4 +- tests/integration/volumes/test_volumes.py | 16 ++++-- .../volumes/test_volumes_resize.py | 7 ++- tests/integration/vpc/test_vpc.py | 16 ++++-- 15 files changed, 125 insertions(+), 65 deletions(-) diff --git a/tests/integration/domains/test_domain_records.py b/tests/integration/domains/test_domain_records.py index 397ef18f0..74eb1e9ff 100644 --- a/tests/integration/domains/test_domain_records.py +++ b/tests/integration/domains/test_domain_records.py @@ -73,8 +73,30 @@ def test_create_a_domain(master_domain): ) output_current = process.stdout.decode() + timestamp = str(time.time_ns()) + # Create domain - domain_id = master_domain + another_domain = ( + exec_test_command( + [ + "linode-cli", + "domains", + "create", + "--type", + "master", + "--domain", + timestamp + "example.com", + "--soa_email", + "pthiel_test@linode.com", + "--text", + "--no-header", + "--format", + "id", + ] + ) + .stdout.decode() + .rstrip() + ) process = exec_test_command( BASE_CMD + ["list", "--format=id", "--text", "--no-header"] @@ -82,10 +104,7 @@ def test_create_a_domain(master_domain): output_after = process.stdout.decode() # Check if list is bigger than previous list - assert ( - len(output_after.splitlines()) > len(output_current.splitlines()), - "the list is not updated with new domain..", - ) + assert len(output_after.splitlines()) > len(output_current.splitlines()) @pytest.mark.smoke @@ -112,9 +131,9 @@ def test_create_domain_srv_record(test_domain_and_record): output = process.stdout.decode() - assert ( - re.search("[0-9]+,SRV,_telnet._tcp,target-test-record+,0,4,4", output), - "Output does not match the format", + assert re.search( + r"[0-9]+,SRV,_telnet\._tcp,target-test-record\.\d+example\.com,0,4,4\n", + str(output), ) @@ -132,9 +151,9 @@ def test_list_srv_record(test_domain_and_record): ) output = process.stdout.decode() - assert ( - re.search("[0-9]+,SRV,_telnet._tcp,record-setup+,0,4,4", output), - "Output does not match the format", + assert re.search( + r"[0-9]+,SRV,_telnet\._tcp,record-setup\.\d+example\.com,0,4,4\n", + str(output), ) @@ -156,9 +175,9 @@ def test_view_domain_record(test_domain_and_record): ) output = process.stdout.decode() - assert ( - re.search("[0-9]+,SRV,_telnet._tcp,record-setup+,0,4,4", output), - "Output does not match the format", + assert re.search( + r"[0-9]+,SRV,_telnet\._tcp,record-setup\.\d+example\.com,0,4,4\n", + output, ) @@ -180,9 +199,9 @@ def test_update_domain_record(test_domain_and_record): ) output = process.stdout.decode() - assert ( - re.search("[0-9]+,SRV,_telnet._tcp,record-setup-update+,0,4,4", output), - "Output does not match the format", + assert re.search( + r"[0-9]+,SRV,_telnet\._tcp,record-setup-update\.\d+example\.com,0,4,4\n", + str(output), ) diff --git a/tests/integration/domains/test_domains_tags.py b/tests/integration/domains/test_domains_tags.py index 7555c6f4d..72d250980 100644 --- a/tests/integration/domains/test_domains_tags.py +++ b/tests/integration/domains/test_domains_tags.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_tag, delete_target_id, @@ -32,7 +33,8 @@ def test_fail_to_create_master_domain_with_invalid_tags(): "--format=id", "--tag", bad_tag, - ] + ], + expected_code=ExitCodes.REQUEST_FAILED, ) @@ -55,7 +57,8 @@ def test_fail_to_create_slave_domain_with_invalid_tags(): "--format=id", "--tag", bad_tag, - ] + ], + expected_code=ExitCodes.REQUEST_FAILED, ) diff --git a/tests/integration/domains/test_master_domains.py b/tests/integration/domains/test_master_domains.py index 2427c3912..ee990b1ab 100644 --- a/tests/integration/domains/test_master_domains.py +++ b/tests/integration/domains/test_master_domains.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -58,7 +59,8 @@ def test_create_domain_fails_without_spcified_type(): "pthiel@linode.com", "--text", "--no-headers", - ] + ], + expected_code=ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result @@ -77,7 +79,8 @@ def test_create_master_domain_fails_without_soa_email(): "example.bc-" + timestamp + ".com", "--text", "--no-headers", - ] + ], + expected_code=ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result diff --git a/tests/integration/firewalls/test_firewalls.py b/tests/integration/firewalls/test_firewalls.py index 989dc8560..495b43bd6 100644 --- a/tests/integration/firewalls/test_firewalls.py +++ b/tests/integration/firewalls/test_firewalls.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -126,7 +127,8 @@ def test_fails_to_create_firewall_without_inbound_policy(): "--no-headers", "--format", "id", - ] + ], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() @@ -151,7 +153,8 @@ def test_fails_to_create_firewall_without_outbound_policy(): "--no-headers", "--format", "id", - ] + ], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() @@ -176,7 +179,8 @@ def test_firewall_label_must_be_unique_upon_creation(test_firewall_id): "--no-headers", "--format", "id", - ] + ], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() diff --git a/tests/integration/kernels/test_kernels.py b/tests/integration/kernels/test_kernels.py index 3195ba3dc..9b6ec0326 100644 --- a/tests/integration/kernels/test_kernels.py +++ b/tests/integration/kernels/test_kernels.py @@ -12,7 +12,7 @@ def test_list_available_kernels(): output = process.stdout.decode() for line in output.splitlines(): - assert ("linode" in line, "Output does not contain keyword linode..") + assert "linode" in line def test_fields_from_kernels_list(): @@ -28,12 +28,9 @@ def test_fields_from_kernels_list(): output = process.stdout.decode() for line in output.splitlines(): - assert ( - re.search( - "linode/.*,.*,(False|True),(i386|x86_64),(False|True),(False|True),.*", - line, - ), - "Output does not match the format specified..", + assert re.search( + "linode/.*,.*,(False|True),(i386|x86_64),(False|True),(False|True),.*", + line, ) @@ -59,14 +56,9 @@ def test_view_kernel(): ) output = process.stdout.decode() - assert ( - "id,version,kvm,architecture,pvops,deprecated,built" in output, - "No header found..", - ) - assert ( - re.search( - "linode/.*,.*,(False|True),(i386|x86_64),(False|True),(False|True),.*", - output, - ), - "Ouput does not match the format specified..", + assert "id,version,kvm,architecture,pvops,deprecated,built" in output + + assert re.search( + "linode/.*,.*,(False|True),(i386|x86_64),(False|True),(False|True),.*", + output, ) diff --git a/tests/integration/linodes/test_linodes.py b/tests/integration/linodes/test_linodes.py index 44dcccc02..4f336363b 100644 --- a/tests/integration/linodes/test_linodes.py +++ b/tests/integration/linodes/test_linodes.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -120,7 +121,8 @@ def test_create_linodes_fails_without_a_root_pass(): DEFAULT_TEST_IMAGE, "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "root_pass root_pass is required" in result diff --git a/tests/integration/linodes/test_power_status.py b/tests/integration/linodes/test_power_status.py index a79b5cc47..9de2dee25 100644 --- a/tests/integration/linodes/test_power_status.py +++ b/tests/integration/linodes/test_power_status.py @@ -65,4 +65,4 @@ def test_shutdown_linode(test_linode_id): result = wait_until(linode_id=linode_id, timeout=180, status="offline") - assert (result, "Linode status has not changed to running from offline") + assert result, "Linode status has not changed to running from offline" diff --git a/tests/integration/linodes/test_rebuild.py b/tests/integration/linodes/test_rebuild.py index adef07f0a..22f0e58a0 100644 --- a/tests/integration/linodes/test_rebuild.py +++ b/tests/integration/linodes/test_rebuild.py @@ -2,6 +2,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -36,7 +37,8 @@ def test_rebuild_fails_without_image(test_linode_id): linode_id, "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result diff --git a/tests/integration/linodes/test_resize.py b/tests/integration/linodes/test_resize.py index 71f2c129f..be096ed13 100644 --- a/tests/integration/linodes/test_resize.py +++ b/tests/integration/linodes/test_resize.py @@ -2,6 +2,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -62,7 +63,15 @@ def test_resize_fails_to_the_same_plan(test_linode_id): result = exec_failing_test_command( BASE_CMD - + ["resize", "--type", linode_plan, "--text", "--no-headers", linode_id] + + [ + "resize", + "--type", + linode_plan, + "--text", + "--no-headers", + linode_id, + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result @@ -97,7 +106,8 @@ def test_resize_fails_to_smaller_plan(test_linode_id): "--text", "--no-headers", linode_id, - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result @@ -120,7 +130,8 @@ def test_resize_fail_to_invalid_plan(test_linode_id): "--text", "--no-headers", linode_id, - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result diff --git a/tests/integration/nodebalancers/test_node_balancers.py b/tests/integration/nodebalancers/test_node_balancers.py index 419d3b9aa..ae6a73141 100644 --- a/tests/integration/nodebalancers/test_node_balancers.py +++ b/tests/integration/nodebalancers/test_node_balancers.py @@ -2,6 +2,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -168,7 +169,8 @@ def create_linode_to_add(linode_cloud_firewall): def test_fail_to_create_nodebalancer_without_region(): result = exec_failing_test_command( - BASE_CMD + ["create", "--text", "--no-headers"] + BASE_CMD + ["create", "--text", "--no-headers"], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "region region is required" in result @@ -217,7 +219,8 @@ def test_display_public_ipv4_for_nodebalancer(test_node_balancers): def test_fail_to_view_nodebalancer_with_invalid_id(): result = exec_failing_test_command( - BASE_CMD + ["view", "535", "--text", "--no-headers"] + BASE_CMD + ["view", "535", "--text", "--no-headers"], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 404" in result @@ -378,7 +381,8 @@ def test_fail_to_update_node_to_public_ipv4_address(test_node_balancers): "--no-headers", "--delimiter", ",", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result diff --git a/tests/integration/stackscripts/test_stackscripts.py b/tests/integration/stackscripts/test_stackscripts.py index a81825c4e..48e3a3660 100644 --- a/tests/integration/stackscripts/test_stackscripts.py +++ b/tests/integration/stackscripts/test_stackscripts.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -108,7 +109,8 @@ def test_test_stackscript_id_fails_without_image(): "--no-headers", "--delimiter", ",", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result @@ -216,7 +218,8 @@ def test_fail_to_deploy_stackscript_to_linode_from_incompatible_image( DEFAULT_RANDOM_PASS, "--no-headers", "--text", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "image is not valid" in result diff --git a/tests/integration/tags/test_tags.py b/tests/integration/tags/test_tags.py index 21db3446f..1aa2adfe0 100644 --- a/tests/integration/tags/test_tags.py +++ b/tests/integration/tags/test_tags.py @@ -2,6 +2,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -38,7 +39,8 @@ def test_view_unique_tag(test_tag_instance): def test_fail_to_create_tag_shorter_than_three_char(): bad_tag = "aa" result = exec_failing_test_command( - BASE_CMD + ["create", "--label", bad_tag, "--text", "--no-headers"] + BASE_CMD + ["create", "--label", bad_tag, "--text", "--no-headers"], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "Length must be 3-50 characters" in result diff --git a/tests/integration/volumes/test_volumes.py b/tests/integration/volumes/test_volumes.py index ed60aedc3..5091b8d16 100644 --- a/tests/integration/volumes/test_volumes.py +++ b/tests/integration/volumes/test_volumes.py @@ -4,6 +4,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -58,7 +59,8 @@ def test_fail_to_create_volume_under_10gb(): "5", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() if "test" == os.environ.get( @@ -80,7 +82,8 @@ def test_fail_to_create_volume_without_region(): "10", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "Must provide a region or a Linode ID" in result @@ -97,7 +100,8 @@ def test_fail_to_create_volume_without_label(): "10", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "label label is required" in result @@ -116,7 +120,8 @@ def test_fail_to_create_volume_over_1024gb_in_size(): "10241", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() if "test" == os.environ.get( "TEST_ENVIRONMENT", None @@ -139,7 +144,8 @@ def test_fail_to_create_volume_with_all_numberic_label(): "10", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result assert "label Must begin with a letter" in result diff --git a/tests/integration/volumes/test_volumes_resize.py b/tests/integration/volumes/test_volumes_resize.py index b5afd1dc8..e79e22ed4 100644 --- a/tests/integration/volumes/test_volumes_resize.py +++ b/tests/integration/volumes/test_volumes_resize.py @@ -3,6 +3,7 @@ import pytest +from linodecli.exit_codes import ExitCodes from tests.integration.helpers import ( delete_target_id, exec_failing_test_command, @@ -49,7 +50,8 @@ def test_resize_fails_to_smaller_volume(test_volume_id): time.sleep(VOLUME_CREATION_WAIT) result = exec_failing_test_command( BASE_CMD - + ["resize", volume_id, "--size", "5", "--text", "--no-headers"] + + ["resize", volume_id, "--size", "5", "--text", "--no-headers"], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in result @@ -67,7 +69,8 @@ def test_resize_fails_to_volume_larger_than_1024gb(test_volume_id): "1024893405", "--text", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() if "test" == os.environ.get( diff --git a/tests/integration/vpc/test_vpc.py b/tests/integration/vpc/test_vpc.py index b5900b54d..5a179e20c 100644 --- a/tests/integration/vpc/test_vpc.py +++ b/tests/integration/vpc/test_vpc.py @@ -1,6 +1,7 @@ import re import time +from linodecli.exit_codes import ExitCodes from tests.integration.conftest import get_regions_with_capabilities from tests.integration.helpers import ( exec_failing_test_command, @@ -160,7 +161,8 @@ def test_fails_to_create_vpc_invalid_label(): res = ( exec_failing_test_command( - BASE_CMD + ["create", "--label", invalid_label, "--region", region] + BASE_CMD + ["create", "--label", invalid_label, "--region", region], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() @@ -184,7 +186,8 @@ def test_fails_to_create_vpc_duplicate_label(test_vpc_wo_subnet): res = ( exec_failing_test_command( - BASE_CMD + ["create", "--label", label, "--region", region] + BASE_CMD + ["create", "--label", label, "--region", region], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() @@ -199,7 +202,8 @@ def test_fails_to_update_vpc_invalid_label(test_vpc_wo_subnet): res = ( exec_failing_test_command( - BASE_CMD + ["update", vpc_id, "--label", invalid_label] + BASE_CMD + ["update", vpc_id, "--label", invalid_label], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() @@ -223,7 +227,8 @@ def test_fails_to_create_vpc_subnet_w_invalid_label(test_vpc_wo_subnet): "--ipv4", "10.1.0.0/24", vpc_id, - ] + ], + ExitCodes.REQUEST_FAILED, ).stderr.decode() assert "Request failed: 400" in res @@ -256,7 +261,8 @@ def test_fails_to_update_vpc_subenet_w_invalid_label(test_vpc_w_subnet): "--text", "--format=label", "--no-headers", - ] + ], + ExitCodes.REQUEST_FAILED, ) .stderr.decode() .rstrip() From ba4addd835c20498f6303e1a1f87ee7341c36c1a Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Fri, 16 Aug 2024 21:26:44 -0400 Subject: [PATCH 02/10] Change Clusters Choice in OBJ Plugin Config to be Text Input (#630) Co-authored-by: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> --- linodecli/plugins/obj/__init__.py | 35 ++++++++----------------------- tests/unit/test_plugin_obj.py | 2 +- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/linodecli/plugins/obj/__init__.py b/linodecli/plugins/obj/__init__.py index f12d53ee9..521d48565 100644 --- a/linodecli/plugins/obj/__init__.py +++ b/linodecli/plugins/obj/__init__.py @@ -18,7 +18,7 @@ from linodecli.cli import CLI from linodecli.configuration import _do_get_request -from linodecli.configuration.helpers import _default_thing_input +from linodecli.configuration.helpers import _default_text_input from linodecli.exit_codes import ExitCodes from linodecli.plugins import PluginContext, inherit_plugin_args from linodecli.plugins.obj.buckets import create_bucket, delete_bucket @@ -63,18 +63,6 @@ HAS_BOTO = False -def get_available_cluster(cli: CLI): - """Get list of possible clusters for the account""" - return [ - c["id"] - for c in _do_get_request( # pylint: disable=protected-access - cli.config.base_url, - "/object-storage/clusters", - token=cli.config.get_token(), - )["data"] - ] - - def generate_url(get_client, args, **kwargs): # pylint: disable=unused-argument """ Generates a URL to an object @@ -291,7 +279,7 @@ def print_help(parser: ArgumentParser): print("See --help for individual commands for more information") -def get_obj_args_parser(clusters: List[str]): +def get_obj_args_parser(): """ Initialize and return the argument parser for the obj plug-in. """ @@ -308,7 +296,6 @@ def get_obj_args_parser(clusters: List[str]): "--cluster", metavar="CLUSTER", type=str, - choices=clusters, help="The cluster to use for the operation", ) @@ -372,8 +359,7 @@ def call( ExitCodes.REQUEST_FAILED ) # requirements not met - we can't go on - clusters = get_available_cluster(context.client) if not is_help else None - parser = get_obj_args_parser(clusters) + parser = get_obj_args_parser() parsed, args = parser.parse_known_args(args) # don't mind --no-defaults if it's there; the top-level parser already took care of it @@ -557,15 +543,12 @@ def _configure_plugin(client: CLI): """ Configures a default cluster value. """ - clusters = get_available_cluster(client) - - cluster = _default_thing_input( # pylint: disable=protected-access - "Configure a default Cluster for operations.", - clusters, - "Default Cluster: ", - "Please select a valid Cluster", - optional=False, # this is the only configuration right now + + cluster = _default_text_input( # pylint: disable=protected-access + "Default cluster for operations (e.g. `us-mia-1`)", + optional=True, ) - client.config.plugin_set_value("cluster", cluster) + if cluster: + client.config.plugin_set_value("cluster", cluster) client.config.write_config() diff --git a/tests/unit/test_plugin_obj.py b/tests/unit/test_plugin_obj.py index 22dfaae31..17ae5c425 100644 --- a/tests/unit/test_plugin_obj.py +++ b/tests/unit/test_plugin_obj.py @@ -5,7 +5,7 @@ def test_print_help(mock_cli: CLI, capsys: CaptureFixture): - parser = get_obj_args_parser(["us-mia-1"]) + parser = get_obj_args_parser() print_help(parser) captured_text = capsys.readouterr() assert parser.format_help() in captured_text.out From 5e6003bcbc060a653abb88ac2d2d10c296fa4d3f Mon Sep 17 00:00:00 2001 From: Vinay <143587840+vshanthe@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:17:09 +0530 Subject: [PATCH 03/10] Test placement (#636) --- tests/integration/conftest.py | 2 +- .../integration/placements/test_placements.py | 136 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/integration/placements/test_placements.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 280bd3b92..2a4f211fa 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -553,7 +553,7 @@ def support_test_linode_id(linode_cloud_firewall): "--type", "g6-nanode-1", "--region", - "us-ord", + "us-mia", "--image", DEFAULT_TEST_IMAGE, "--label", diff --git a/tests/integration/placements/test_placements.py b/tests/integration/placements/test_placements.py new file mode 100644 index 000000000..7fa2309da --- /dev/null +++ b/tests/integration/placements/test_placements.py @@ -0,0 +1,136 @@ +import time + +import pytest + +from tests.integration.helpers import ( + assert_headers_in_lines, + delete_target_id, + exec_test_command, +) + +BASE_CMD = ["linode-cli", "placement"] + + +@pytest.fixture +def create_placement_group(): + new_label = str(time.time_ns()) + "label" + placement_group_id = ( + exec_test_command( + BASE_CMD + + [ + "group-create", + "--label", + new_label, + "--region", + "us-mia", + "--placement_group_type", + "anti_affinity:local", + "--placement_group_policy", + "strict", + "--text", + "--no-headers", + "--format=id", + ] + ) + .stdout.decode() + .rstrip() + ) + yield placement_group_id + delete_target_id( + target="placement", subcommand="group-delete", id=placement_group_id + ) + + +def test_placement_group_list(): + res = ( + exec_test_command(BASE_CMD + ["groups-list", "--text", "--delimiter=,"]) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + headers = ["placement_group_type", "region", "label"] + assert_headers_in_lines(headers, lines) + + +def test_placement_group_view(create_placement_group): + placement_group_id = create_placement_group + res = ( + exec_test_command( + BASE_CMD + + ["group-view", placement_group_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + + headers = ["placement_group_type", "region", "label"] + assert_headers_in_lines(headers, lines) + + +@pytest.mark.skip(reason="BUG TPT-3109") +def test_assign_placement_group(support_test_linode_id, create_placement_group): + linode_id = support_test_linode_id + placement_group_id = create_placement_group + res = ( + exec_test_command( + BASE_CMD + + [ + "assign-linode", + placement_group_id, + "--linodes", + linode_id, + "--text", + "--delimiter=,", + ] + ) + .stdout.decode() + .rstrip() + ) + assert placement_group_id in res + + +@pytest.mark.skip(reason="BUG TPT-3109") +def test_unassign_placement_group( + support_test_linode_id, create_placement_group +): + linode_id = support_test_linode_id + placement_group_id = create_placement_group + res = ( + exec_test_command( + BASE_CMD + + [ + "unassign-linode", + placement_group_id, + "--linode", + linode_id, + "--text", + "--delimiter=,", + ] + ) + .stdout.decode() + .rstrip() + ) + assert placement_group_id not in res + + +def test_update_placement_group(create_placement_group): + placement_group_id = create_placement_group + new_label = str(time.time_ns()) + "label" + updated_label = ( + exec_test_command( + BASE_CMD + + [ + "group-update", + placement_group_id, + "--label", + new_label, + "--text", + "--no-headers", + "--format=label", + ] + ) + .stdout.decode() + .rstrip() + ) + assert new_label == updated_label From c0a860ca283f1dea20be176af91eabda4203f25c Mon Sep 17 00:00:00 2001 From: Jacob Riddle <87780794+jriddle-linode@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:45:30 -0400 Subject: [PATCH 04/10] Add suppress-version-warning config option (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 Description **What does this PR do and why is this change necessary?** Adds `get_bool('value')` method for configuration options and adds a step in configuration to support `suppress-version-warning`. Also adds support for `LINODE_CLI_SUPPRESS_VERSION_WARNING` environment variable. ## ✔️ How to Test **What are the steps to reproduce the issue or verify the changes?** 1. Install the new version of the CLI ```bash make install ``` 2. Go through the configuration process ```bash lin configure ``` 3. The last question should ask you about suppressing API Version Warnings 4. Use the CLI and see if you view this warning ### Environment variable 1. Install the new CLI or delete the value from your config 2. Use the cli after setting the environment variable ```bash export LINODE_CLI_SUPPRESS_VERSION_WARNING=true lin linodes ls unset $LINODE_CLI_SUPPRESS_VERSION_WARNING lin linodes ls ``` 3. Verify the output doesn't have the error. **How do I run the relevant unit/integration tests?** ```bash make testunit ``` resolves #582 --- linodecli/api_request.py | 7 +++++-- linodecli/configuration/config.py | 27 +++++++++++++++++++++++++++ tests/unit/test_configuration.py | 12 +++++++++++- wiki/Configuration.md | 3 +++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/linodecli/api_request.py b/linodecli/api_request.py index d3bf909f1..a0e63137e 100644 --- a/linodecli/api_request.py +++ b/linodecli/api_request.py @@ -4,6 +4,7 @@ import itertools import json +import os import sys import time from typing import Any, Iterable, List, Optional @@ -373,8 +374,10 @@ def _attempt_warn_old_version(ctx, result): "with --suppress-warnings", file=sys.stderr, ) - - if new_version_exists: + suppress_version_warning = ctx.config.get_bool("suppress-version-warning") or os.getenv( + "LINODE_CLI_SUPPRESS_VERSION_WARNING" + ) + if new_version_exists and not suppress_version_warning: print( f"The API responded with version {spec_version}, which is newer than " f"the CLI's version of {ctx.spec_version}. Please update the CLI to get " diff --git a/linodecli/configuration/config.py b/linodecli/configuration/config.py index 48226a148..88c557bd4 100644 --- a/linodecli/configuration/config.py +++ b/linodecli/configuration/config.py @@ -186,6 +186,30 @@ def get_value(self, key: str) -> Optional[Any]: return self.config.get(username, key) + def get_bool(self, key: str) -> bool: + """ + Retrieves and returns an existing config boolean for the current user. This + is intended for plugins to use instead of having to deal with figuring out + who the current user is when accessing their config. + + .. warning:: + Plugins _MUST NOT_ set values for the user's config except through + ``plugin_set_value`` below. + + :param key: The key to look up. + :type key: str + + :returns: The boolean for that key, or False if the key doesn't exist for the + current user. + :rtype: any + """ + username = self.username or self.default_username() + + if not self.config.has_option(username, key): + return False + + return self.config.getboolean(username, key) + # plugin methods - these are intended for plugins to utilize to store their # own persistent config information def plugin_set_value(self, key: str, value: Any): @@ -449,6 +473,9 @@ def configure( if _bool_input("Configure a custom API target?", default=False): self._configure_api_target(config) + if _bool_input("Suppress API Version Warnings?", default=False): + config["suppress-version-warning"] = "true" + # save off the new configuration if username != "DEFAULT" and not self.config.has_section(username): self.config.add_section(username) diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index ff85944cc..2c43a078c 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -38,6 +38,7 @@ class TestConfiguration: plugin-testplugin-testkey = plugin-test-value authorized_users = cli-dev mysql_engine = mysql/8.0.26 +suppress-version-warning = true [cli-dev2] token = {test_token}2 @@ -156,6 +157,14 @@ def test_get_value(self): assert conf.get_value("notakey") == None assert conf.get_value("region") == "us-east" + def test_get_bool(self): + """ + Test CLIConfig.get_bool({key}) + """ + conf = self._build_test_config() + assert conf.get_bool("notakey") == False + assert conf.get_bool("suppress-version-warning") == True + def test_plugin_set_value(self): """ Test CLIConfig.plugin_set_value({key}, {value}) @@ -265,6 +274,7 @@ def test_configure_no_default_terminal(self): "foobar.linode.com", "v4beta", "https", + "n", ] ) @@ -319,7 +329,7 @@ def test_configure_default_terminal(self): """ conf = configuration.CLIConfig(self.base_url, skip_config=True) - answers = iter(["1", "1", "1", "1", "1", "1", "n"]) + answers = iter(["1", "1", "1", "1", "1", "1", "n", "n"]) def mock_input(prompt): if not prompt: diff --git a/wiki/Configuration.md b/wiki/Configuration.md index 879dc6d18..ed595da48 100644 --- a/wiki/Configuration.md +++ b/wiki/Configuration.md @@ -38,6 +38,9 @@ without having a configuration file, which is desirable in some situations. You may also specify the path to a custom Certificate Authority file using the `LINODE_CLI_CA` environment variable. +If you wish to hide the API Version warning you can use the `LINODE_CLI_SUPPRESS_VERSION_WARNING` +environment variable. + ## Configurable API URL In some cases you may want to run linode-cli against a non-default Linode API URL. From 04c1e92377962d496197437451812627b725de3f Mon Sep 17 00:00:00 2001 From: Lena Garber <114949949+lgarber-akamai@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:06:26 -0400 Subject: [PATCH 05/10] new: Break out command help page logic; sort actions in output (#638) --- linodecli/__init__.py | 19 ++--------------- linodecli/help_pages.py | 35 +++++++++++++++++++++++++++++-- tests/unit/conftest.py | 20 ++++++++++++++++++ tests/unit/test_help_pages.py | 39 +++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/linodecli/__init__.py b/linodecli/__init__.py index aa7331b40..776b6589f 100755 --- a/linodecli/__init__.py +++ b/linodecli/__init__.py @@ -27,6 +27,7 @@ from .help_pages import ( HELP_TOPICS, print_help_action, + print_help_command_actions, print_help_commands, print_help_default, print_help_env_vars, @@ -224,23 +225,7 @@ def main(): # pylint: disable=too-many-branches,too-many-statements and parsed.action is None and parsed.command in cli.ops ): - print(f"linode-cli {parsed.command} [ACTION]") - print() - print("Available actions: ") - - content = [ - [", ".join([action, *op.action_aliases]), op.summary] - for action, op in cli.ops[parsed.command].items() - ] - - table = Table( - Column(header="action", no_wrap=True), - Column(header="summary", style="cyan"), - ) - for row in content: - table.add_row(*row) - - rprint(table) + print_help_command_actions(cli.ops, parsed.command) sys.exit(ExitCodes.SUCCESS) if parsed.command is not None and parsed.action is not None: diff --git a/linodecli/help_pages.py b/linodecli/help_pages.py index ed4be7aa4..c249dd8fb 100644 --- a/linodecli/help_pages.py +++ b/linodecli/help_pages.py @@ -3,15 +3,16 @@ help pages. """ +import sys import textwrap from collections import defaultdict -from typing import List, Optional +from typing import Dict, List, Optional from rich import box from rich import print as rprint from rich.console import Console from rich.padding import Padding -from rich.table import Table +from rich.table import Column, Table from rich.text import Text from linodecli import plugins @@ -140,6 +141,36 @@ def print_help_default(): ) +def print_help_command_actions( + ops: Dict[str, Dict[str, OpenAPIOperation]], + command: Optional[str], + file=sys.stdout, +): + """ + Prints the help page for a single command, including all actions + under the given command. + + :param ops: A dictionary mapping CLI commands -> actions -> operations. + :param command: The command to print the help page for. + """ + + print(f"linode-cli {command} [ACTION]\n\nAvailable actions: ", file=file) + + content = [ + [", ".join([action, *op.action_aliases]), op.summary] + for action, op in sorted(ops[command].items(), key=lambda v: v[0]) + ] + + table = Table( + Column(header="action", no_wrap=True), + Column(header="summary", style="cyan"), + ) + for row in content: + table.add_row(*row) + + rprint(table, file=file) + + def print_help_action( cli: "CLI", command: Optional[str], action: Optional[str] ): diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 0d6031793..fef49ab27 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,4 +1,5 @@ import configparser +from typing import List import pytest from openapi3 import OpenAPI @@ -321,3 +322,22 @@ def write_config(self): # pylint: disable=missing-function-docstring pass return Config() + + +def assert_contains_ordered_substrings(target: str, entries: List[str]): + """ + Asserts whether the given string contains the given entries in order, + ignoring any irrelevant characters in-between. + + :param target: The string to search. + :param entries: The ordered list of entries to search for. + """ + + start_index = 0 + + for entry in entries: + find_index = target[start_index:].find(entry) + assert find_index >= 0 + + # Search for the next entry after the end of this entry + start_index = find_index + len(entry) diff --git a/tests/unit/test_help_pages.py b/tests/unit/test_help_pages.py index 3c6fc6474..96d1ea3b1 100644 --- a/tests/unit/test_help_pages.py +++ b/tests/unit/test_help_pages.py @@ -1,6 +1,8 @@ +from io import StringIO from types import SimpleNamespace from linodecli import help_pages +from tests.unit.conftest import assert_contains_ordered_substrings class TestHelpPages: @@ -175,3 +177,40 @@ def test_action_help_post_method(self, capsys, mocker, mock_cli): assert "(required, nullable, conflicts with children)" in captured.out assert "(JSON, nullable, conflicts with children)" in captured.out assert "filter results" not in captured.out + + def test_help_command_actions(self, mocker): + test_operations = { + "foo": { + "b-create": mocker.MagicMock( + summary="Test summary.", action_aliases=[] + ), + "b-list": mocker.MagicMock( + summary="Test summary 2.", action_aliases=["b-ls"] + ), + "a-list": mocker.MagicMock( + summary="Test summary 3.", action_aliases=["a-ls"] + ), + } + } + + stdout_buffer = StringIO() + help_pages.print_help_command_actions( + test_operations, "foo", file=stdout_buffer + ) + + # Ensure the given snippets are printed in order, ignoring irrelevant characters + assert_contains_ordered_substrings( + stdout_buffer.getvalue(), + [ + "linode-cli foo [ACTION]", + "Available actions:", + "action", + "summary", + "a-list, a-ls", + "Test summary 3.", + "b-create", + "Test summary.", + "b-list, b-ls", + "Test summary 2.", + ], + ) From 7886c08693e1d9f50f8b6b1dddecf2a1ece19be7 Mon Sep 17 00:00:00 2001 From: Vinay <143587840+vshanthe@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:51:47 +0530 Subject: [PATCH 06/10] Add lke tests (#641) --- linodecli/api_request.py | 6 +- tests/integration/lke/test_clusters.py | 199 ++++++++++++++++++++++++- 2 files changed, 197 insertions(+), 8 deletions(-) diff --git a/linodecli/api_request.py b/linodecli/api_request.py index a0e63137e..f73b1c883 100644 --- a/linodecli/api_request.py +++ b/linodecli/api_request.py @@ -374,9 +374,9 @@ def _attempt_warn_old_version(ctx, result): "with --suppress-warnings", file=sys.stderr, ) - suppress_version_warning = ctx.config.get_bool("suppress-version-warning") or os.getenv( - "LINODE_CLI_SUPPRESS_VERSION_WARNING" - ) + suppress_version_warning = ctx.config.get_bool( + "suppress-version-warning" + ) or os.getenv("LINODE_CLI_SUPPRESS_VERSION_WARNING") if new_version_exists and not suppress_version_warning: print( f"The API responded with version {spec_version}, which is newer than " diff --git a/tests/integration/lke/test_clusters.py b/tests/integration/lke/test_clusters.py index 368cb579e..c4325fd30 100644 --- a/tests/integration/lke/test_clusters.py +++ b/tests/integration/lke/test_clusters.py @@ -55,13 +55,30 @@ def test_deploy_an_lke_cluster(): "--no-defaults", ] ).stdout.decode() - assert label + ",us-ord," + lke_version in result - # Sleep needed here for proper deletion of linodes that are related to lke cluster - time.sleep(15) - remove_lke_clusters() +@pytest.fixture +def get_cluster_id(): + cluster_id = ( + exec_test_command( + BASE_CMD + + [ + "clusters-list", + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ) + .stdout.decode() + .rstrip() + .splitlines() + ) + first_id = cluster_id[0] + yield first_id def test_lke_cluster_list(): @@ -78,6 +95,175 @@ def test_lke_cluster_list(): assert_headers_in_lines(headers, lines) +def test_view_lke_cluster(get_cluster_id): + cluster_id = get_cluster_id + + res = ( + exec_test_command( + BASE_CMD + ["cluster-view", cluster_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + headers = ["label", "k8s_version"] + assert_headers_in_lines(headers, lines) + + +def test_update_kubernetes_cluster(get_cluster_id): + cluster_id = get_cluster_id + new_label = "cluster_test" + str(time.time_ns()) + updated_label = ( + exec_test_command( + BASE_CMD + + [ + "cluster-update", + cluster_id, + "--label", + new_label, + "--text", + "--no-headers", + "--format=label", + ] + ) + .stdout.decode() + .rstrip() + ) + assert new_label == updated_label + + +def test_list_kubernetes_endpoint(get_cluster_id): + cluster_id = get_cluster_id + res = ( + exec_test_command( + BASE_CMD + + ["api-endpoints-list", cluster_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + + headers = ["endpoint"] + assert_headers_in_lines(headers, lines) + + +def test_cluster_dashboard_url(get_cluster_id): + cluster_id = get_cluster_id + res = ( + exec_test_command( + BASE_CMD + + ["cluster-dashboard-url", cluster_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + + headers = ["url"] + assert_headers_in_lines(headers, lines) + + +@pytest.fixture +def get_node_pool_id(get_cluster_id): + cluster_id = get_cluster_id + nodepool_id = ( + exec_test_command( + BASE_CMD + + [ + "pools-list", + cluster_id, + "--text", + "--no-headers", + "--delimiter", + ",", + "--format", + "id", + ] + ) + .stdout.decode() + .rstrip() + .splitlines() + ) + first_id = nodepool_id[0] + yield first_id + + +def test_node_pool_list(get_cluster_id): + cluster_id = get_cluster_id + res = ( + exec_test_command( + BASE_CMD + ["pools-list", cluster_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + + headers = ["nodes.id", "nodes.instance_id"] + assert_headers_in_lines(headers, lines) + + +def test_view_pool(get_cluster_id, get_node_pool_id): + cluster_id = get_cluster_id + node_pool_id = get_node_pool_id + res = ( + exec_test_command( + BASE_CMD + + ["pool-view", cluster_id, node_pool_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + headers = ["type", "labels.value"] + assert_headers_in_lines(headers, lines) + + +@pytest.mark.skip(reason="BUG TPT-TPT-3145") +def test_update_node_pool(get_cluster_id, get_node_pool_id): + cluster_id = get_cluster_id + node_pool_id = get_node_pool_id + new_label = "cluster_test" + str(time.time_ns()) + updated_count = ( + exec_test_command( + BASE_CMD + + [ + "pool-update", + cluster_id, + node_pool_id, + "--count", + "5", + "--label.value", + new_label, + "--text", + "--no-headers", + "--format=label", + ] + ) + .stdout.decode() + .rstrip() + ) + assert new_label == updated_count + + +@pytest.mark.skip(reason="BUG TPT-TPT-3145") +def test_view_node(get_cluster_id, get_node_pool_id): + cluster_id = get_cluster_id + node_pool_id = get_node_pool_id + res = ( + exec_test_command( + BASE_CMD + + ["node-view", cluster_id, node_pool_id, "--text", "--delimiter=,"] + ) + .stdout.decode() + .rstrip() + ) + lines = res.splitlines() + headers = ["type", "labels.value"] + assert_headers_in_lines(headers, lines) + + @pytest.fixture def test_version_id(): version_id = ( @@ -101,7 +287,7 @@ def test_version_id(): yield first_id -def test_beta_view(test_version_id): +def test_version_view(test_version_id): version_id = test_version_id res = ( exec_test_command( @@ -114,3 +300,6 @@ def test_beta_view(test_version_id): headers = ["id"] assert_headers_in_lines(headers, lines) + # Sleep needed here for proper deletion of linodes that are related to lke cluster + time.sleep(5) + remove_lke_clusters() From b85cc93d4bf7f076e982a3e1ccc27ba2fb9ce69e Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:01:19 -0700 Subject: [PATCH 07/10] test: Update volume tests to reflect new threshold values for sizing (#646) --- tests/integration/volumes/test_volumes.py | 18 ++++++++++-------- .../integration/volumes/test_volumes_resize.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/integration/volumes/test_volumes.py b/tests/integration/volumes/test_volumes.py index 5091b8d16..4db31850c 100644 --- a/tests/integration/volumes/test_volumes.py +++ b/tests/integration/volumes/test_volumes.py @@ -9,22 +9,24 @@ delete_target_id, exec_failing_test_command, exec_test_command, + get_random_text, ) BASE_CMD = ["linode-cli", "volumes"] -timestamp = str(time.time_ns()) +label = get_random_text(8) unique_tag = str(time.time_ns()) + "-tag" @pytest.fixture(scope="package") def test_volume_id(): + label = get_random_text(8) volume_id = ( exec_test_command( BASE_CMD + [ "create", "--label", - "A" + timestamp, + label, "--region", "us-ord", "--size", @@ -52,7 +54,7 @@ def test_fail_to_create_volume_under_10gb(): + [ "create", "--label", - "A" + timestamp, + label, "--region", "us-ord", "--size", @@ -68,7 +70,7 @@ def test_fail_to_create_volume_under_10gb(): ) or "dev" == os.environ.get("TEST_ENVIRONMENT", None): assert "size Must be 10-1024" in result else: - assert "size Must be 10-10240" in result + assert "size Must be 10-16384" in result def test_fail_to_create_volume_without_region(): @@ -77,7 +79,7 @@ def test_fail_to_create_volume_without_region(): + [ "create", "--label", - "A" + timestamp, + label, "--size", "10", "--text", @@ -113,11 +115,11 @@ def test_fail_to_create_volume_over_1024gb_in_size(): + [ "create", "--label", - "A" + timestamp, + label, "--region", "us-ord", "--size", - "10241", + "19000", "--text", "--no-headers", ], @@ -128,7 +130,7 @@ def test_fail_to_create_volume_over_1024gb_in_size(): ) or "dev" == os.environ.get("TEST_ENVIRONMENT", None): assert "size Must be 10-1024" in result else: - assert "size Must be 10-10240" in result + assert "size Must be 10-16384" in result def test_fail_to_create_volume_with_all_numberic_label(): diff --git a/tests/integration/volumes/test_volumes_resize.py b/tests/integration/volumes/test_volumes_resize.py index e79e22ed4..b3f8f2f38 100644 --- a/tests/integration/volumes/test_volumes_resize.py +++ b/tests/integration/volumes/test_volumes_resize.py @@ -82,7 +82,7 @@ def test_resize_fails_to_volume_larger_than_1024gb(test_volume_id): ) else: assert ( - "Storage volumes cannot be resized larger than 10240 gigabytes" + "Storage volumes cannot be resized larger than 16384 gigabytes" in result ) From 4a3f37dada4cb414a4992fa517716dfba82ffea6 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 25 Sep 2024 10:09:25 -0700 Subject: [PATCH 08/10] test: Add retry on flaky tests (#647) --- pyproject.toml | 3 ++- tests/integration/linodes/test_power_status.py | 4 ++-- tests/integration/linodes/test_rebuild.py | 1 + tests/integration/linodes/test_resize.py | 1 + tests/integration/lke/test_clusters.py | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8cba2146a..311402da7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ dev = [ "requests-mock==1.12.1", "boto3-stubs[s3]", "build>=0.10.0", - "twine>=4.0.2" + "twine>=4.0.2", + "pytest-rerunfailures" ] [project.scripts] diff --git a/tests/integration/linodes/test_power_status.py b/tests/integration/linodes/test_power_status.py index 9de2dee25..2ffb57f3a 100644 --- a/tests/integration/linodes/test_power_status.py +++ b/tests/integration/linodes/test_power_status.py @@ -3,7 +3,6 @@ from tests.integration.helpers import delete_target_id, exec_test_command from tests.integration.linodes.helpers_linodes import ( BASE_CMD, - create_linode, create_linode_and_wait, wait_until, ) @@ -11,7 +10,7 @@ @pytest.fixture def test_linode_id(linode_cloud_firewall): - linode_id = create_linode(firewall_id=linode_cloud_firewall) + linode_id = create_linode_and_wait(firewall_id=linode_cloud_firewall) yield linode_id @@ -52,6 +51,7 @@ def test_reboot_linode(create_linode_in_running_state): ), "Linode status has not changed to running from provisioning" +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_shutdown_linode(test_linode_id): linode_id = test_linode_id diff --git a/tests/integration/linodes/test_rebuild.py b/tests/integration/linodes/test_rebuild.py index 22f0e58a0..82d7e20fb 100644 --- a/tests/integration/linodes/test_rebuild.py +++ b/tests/integration/linodes/test_rebuild.py @@ -25,6 +25,7 @@ def test_linode_id(linode_cloud_firewall): delete_target_id(target="linodes", id=linode_id) +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_rebuild_fails_without_image(test_linode_id): linode_id = test_linode_id diff --git a/tests/integration/linodes/test_resize.py b/tests/integration/linodes/test_resize.py index be096ed13..fc573ba36 100644 --- a/tests/integration/linodes/test_resize.py +++ b/tests/integration/linodes/test_resize.py @@ -42,6 +42,7 @@ def test_linode_id(linode_cloud_firewall): delete_target_id(target="linodes", id=linode_id) +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_resize_fails_to_the_same_plan(test_linode_id): linode_id = test_linode_id linode_plan = ( diff --git a/tests/integration/lke/test_clusters.py b/tests/integration/lke/test_clusters.py index c4325fd30..8c0d24022 100644 --- a/tests/integration/lke/test_clusters.py +++ b/tests/integration/lke/test_clusters.py @@ -132,6 +132,7 @@ def test_update_kubernetes_cluster(get_cluster_id): assert new_label == updated_label +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_list_kubernetes_endpoint(get_cluster_id): cluster_id = get_cluster_id res = ( From e7efcea20369183a40ea7778bc6a87eeb446ff43 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:38:00 -0700 Subject: [PATCH 09/10] test: fix unit tests to not overwrite existing cli config file (#642) --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 69720a1ab..a8884fdf2 100644 --- a/Makefile +++ b/Makefile @@ -66,9 +66,12 @@ clean: rm -rf dist linode_cli.egg-info build .PHONY: testunit -testunit: export LINODE_CLI_TEST_MODE = 1 testunit: - pytest -v tests/unit + @mkdir -p /tmp/linode/.config + @orig_xdg_config_home=$${XDG_CONFIG_HOME:-}; \ + export LINODE_CLI_TEST_MODE=1 XDG_CONFIG_HOME=/tmp/linode/.config; \ + pytest -v tests/unit; \ + export XDG_CONFIG_HOME=$$orig_xdg_config_home .PHONY: testint testint: From 9d99566c96c8a3dc39d0081c101863c710558ca4 Mon Sep 17 00:00:00 2001 From: Youjung Kim <126618609+ykim-1@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:50:40 -0700 Subject: [PATCH 10/10] test: Update smoke test coverage and improve nightly test workflow (#637) --- .github/workflows/e2e-suite.yml | 68 ++++++++++++++++- .github/workflows/nightly-smoke-tests.yml | 75 ++++++++++++++++++- .pylintrc | 3 +- e2e_scripts | 2 +- .../integration/domains/test_slave_domains.py | 5 +- tests/integration/linodes/test_types.py | 3 + tests/integration/longview/test_longview.py | 9 ++- .../regions/test_plugin_region_table.py | 2 + tests/integration/vpc/test_vpc.py | 4 + 9 files changed, 160 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e-suite.yml b/.github/workflows/e2e-suite.yml index 6a2b39b93..d1b8b91fa 100644 --- a/.github/workflows/e2e-suite.yml +++ b/.github/workflows/e2e-suite.yml @@ -23,7 +23,7 @@ on: - dev jobs: - integration-tests: + integration_tests: name: Run integration tests on Ubuntu runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' && inputs.sha != '' || github.event_name == 'push' || github.event_name == 'pull_request' @@ -167,3 +167,69 @@ jobs: conclusion: process.env.conclusion }); return result; + + notify-slack: + runs-on: ubuntu-latest + needs: [integration_tests] + if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository + + steps: + - name: Notify Slack + uses: slackapi/slack-github-action@v1.27.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Build Result:*\n${{ steps.integration_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\n`${{ github.ref_name }}`" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + }, + { + "type": "mrkdwn", + "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index 76b725720..baa498028 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -4,16 +4,25 @@ on: schedule: - cron: "0 0 * * *" workflow_dispatch: + inputs: + sha: + description: 'Commit SHA to test' + required: false + default: '' + type: string jobs: smoke_tests: + if: github.repository == 'linode/linode-cli' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: - ref: dev + fetch-depth: 0 + submodules: 'recursive' + ref: ${{ github.event.inputs.sha || github.ref }} - name: Set up Python uses: actions/setup-python@v4 @@ -29,7 +38,69 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run smoke tests + id: smoke_tests run: | make smoketest env: LINODE_CLI_TOKEN: ${{ secrets.LINODE_TOKEN }} + + - name: Notify Slack + if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository + uses: slackapi/slack-github-action@v1.26.0 + with: + channel-id: ${{ secrets.SLACK_CHANNEL_ID }} + payload: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":rocket: *${{ github.workflow }} Completed in: ${{ github.repository }}* :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Build Result:*\n${{ steps.smoke_tests.outcome == 'success' && ':large_green_circle: Build Passed' || ':red_circle: Build Failed' }}" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\n`${{ github.ref_name }}`" + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Commit Hash:*\n<${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.sha }}>" + }, + { + "type": "mrkdwn", + "text": "*Run URL:*\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run Details>" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Triggered by: :bust_in_silhouette: `${{ github.actor }}`" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.pylintrc b/.pylintrc index 019af5118..03ddde567 100644 --- a/.pylintrc +++ b/.pylintrc @@ -424,7 +424,8 @@ disable=raw-checker-failed, duplicate-code, too-few-public-methods, too-many-instance-attributes, - use-symbolic-message-instead + use-symbolic-message-instead, + too-many-positional-arguments # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/e2e_scripts b/e2e_scripts index b56178520..6b71cb72e 160000 --- a/e2e_scripts +++ b/e2e_scripts @@ -1 +1 @@ -Subproject commit b56178520fae446a0a4f38df6259deb845efa667 +Subproject commit 6b71cb72eb20a18ace82f9e73a0f99fe1141d625 diff --git a/tests/integration/domains/test_slave_domains.py b/tests/integration/domains/test_slave_domains.py index dcc1faee7..15fbeed31 100644 --- a/tests/integration/domains/test_slave_domains.py +++ b/tests/integration/domains/test_slave_domains.py @@ -102,7 +102,4 @@ def test_update_slave_domain(slave_domain_setup): ] ) - assert ( - result.returncode == SUCCESS_STATUS_CODE, - "Failed to update slave domain", - ) + assert result.returncode == SUCCESS_STATUS_CODE diff --git a/tests/integration/linodes/test_types.py b/tests/integration/linodes/test_types.py index 79f117730..5cb962c86 100644 --- a/tests/integration/linodes/test_types.py +++ b/tests/integration/linodes/test_types.py @@ -2,6 +2,8 @@ import subprocess from typing import List +import pytest + env = os.environ.copy() env["COLUMNS"] = "200" @@ -16,6 +18,7 @@ def exec_test_command(args: List[str]): # verifying the DC pricing changes along with types +@pytest.mark.smoke def test_linode_type(): process = exec_test_command(["linode-cli", "linodes", "types"]) output = process.stdout.decode() diff --git a/tests/integration/longview/test_longview.py b/tests/integration/longview/test_longview.py index fd766cf00..a332874c2 100644 --- a/tests/integration/longview/test_longview.py +++ b/tests/integration/longview/test_longview.py @@ -11,9 +11,10 @@ BASE_CMD = ["linode-cli", "longview"] +@pytest.mark.smoke def test_create_longview_client(): new_label = str(time.time_ns()) + "label" - exec_test_command( + result = exec_test_command( BASE_CMD + [ "create", @@ -21,8 +22,12 @@ def test_create_longview_client(): new_label, "--text", "--no-headers", + "--delimiter", + ",", ] - ) + ).stdout.decode() + + assert new_label in result def test_longview_client_list(): diff --git a/tests/integration/regions/test_plugin_region_table.py b/tests/integration/regions/test_plugin_region_table.py index 8d15788b3..0d4eade82 100644 --- a/tests/integration/regions/test_plugin_region_table.py +++ b/tests/integration/regions/test_plugin_region_table.py @@ -44,6 +44,7 @@ def test_regions_list(): assert_headers_in_lines(headers, lines) +@pytest.mark.smoke def test_regions_list_avail(): res = ( exec_test_command(BASE_CMD + ["list-avail", "--text", "--delimiter=,"]) @@ -78,6 +79,7 @@ def get_region_id(): yield first_id +@pytest.mark.smoke def test_regions_view(get_region_id): region_id = get_region_id res = ( diff --git a/tests/integration/vpc/test_vpc.py b/tests/integration/vpc/test_vpc.py index 5a179e20c..f03e5fdce 100644 --- a/tests/integration/vpc/test_vpc.py +++ b/tests/integration/vpc/test_vpc.py @@ -1,6 +1,8 @@ import re import time +import pytest + from linodecli.exit_codes import ExitCodes from tests.integration.conftest import get_regions_with_capabilities from tests.integration.helpers import ( @@ -35,6 +37,7 @@ def test_view_vpc(test_vpc_wo_subnet): assert vpc_id in res +@pytest.mark.smoke def test_update_vpc(test_vpc_wo_subnet): vpc_id = test_vpc_wo_subnet @@ -120,6 +123,7 @@ def test_view_subnet(test_vpc_wo_subnet, test_subnet): assert vpc_subnet_id in output +@pytest.mark.smoke def test_update_subnet(test_vpc_w_subnet): vpc_id = test_vpc_w_subnet