From 5e3537d14a59e8e2c082278e5b82cd9c48bf677f Mon Sep 17 00:00:00 2001 From: Lena Garber Date: Thu, 17 Aug 2023 11:16:43 -0400 Subject: [PATCH] Add tests --- Makefile | 2 +- linodecli/output.py | 24 ++++- tests/fixtures/subtable_test_get.yaml | 56 ++++++++++ tests/unit/conftest.py | 43 ++++++++ tests/unit/test_output.py | 146 +++++++++++++++++++++++--- 5 files changed, 251 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/subtable_test_get.yaml diff --git a/Makefile b/Makefile index e798ba420..4e53c7541 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ clean: .PHONY: testunit testunit: export LINODE_CLI_TEST_MODE = 1 testunit: - pytest tests/unit + pytest -v tests/unit .PHONY: testint testint: diff --git a/linodecli/output.py b/linodecli/output.py index 554553fd5..d0dab070a 100644 --- a/linodecli/output.py +++ b/linodecli/output.py @@ -92,6 +92,12 @@ def print( ), } + if len(columns) < 1: + raise ValueError( + "Expected a non-zero number of columns." + "This is always an error in the OpenAPI spec." + ) + if isinstance(columns[0], OpenAPIResponseAttr): header = [c.column_name for c in columns] else: @@ -148,7 +154,7 @@ def _pop_attrs_for_subtable( Pops all attributes that belong to the given subtable and returns them. """ - results = [v for v in attrs if table + "." in v.name] + results = [v for v in attrs if v.name.startswith(table + ".")] # Drop the corresponding entries from the root attrs for v in results: @@ -169,7 +175,7 @@ def _scope_data_to_subtable(data: List[Dict[str, Any]], table: str) -> Any: if len(data) == 0: return data - result = data[0] + result = data[0] if isinstance(data, list) else data for seg in table.split("."): if seg not in result: @@ -245,6 +251,7 @@ def _table_output( if title is not None: tab.title = title + tab.min_width = self.column_width or len(title) rprint(tab, file=to) @@ -291,6 +298,7 @@ def _select_json_elements(keys, json_res): paths to handle nested dicts """ ret = {} + for k, v in json_res.items(): if k in keys: ret[k] = v @@ -298,6 +306,18 @@ def _select_json_elements(keys, json_res): v = OutputHandler._select_json_elements(keys, v) if v: ret[k] = v + elif isinstance(v, list): + results = [] + for elem in v: + selected = OutputHandler._select_json_elements(keys, elem) + if not selected: + continue + + results.append(selected) + + if len(results) > 0: + ret[k] = results + return ret def _build_output_content( diff --git a/tests/fixtures/subtable_test_get.yaml b/tests/fixtures/subtable_test_get.yaml new file mode 100644 index 000000000..a53128dd4 --- /dev/null +++ b/tests/fixtures/subtable_test_get.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.1 +info: + title: API Specification + version: 1.0.0 +servers: + - url: http://localhost + +paths: + /foo/bar: + get: + summary: get info with a complex structure + operationId: fooBarGet + description: This is description + responses: + '200': + description: Successful response + content: + application/json: + x-linode-cli-subtables: + - table + - foo.table + - foo.single_nested + schema: + type: object + properties: + table: + type: array + items: + type: object + properties: + foo: + type: string + bar: + type: integer + foo: + type: object + properties: + single_nested: + type: object + properties: + foo: + type: string + bar: + type: string + table: + type: array + items: + type: object + properties: + foobar: + type: array + format: ipv4 + items: + type: string + foobar: + type: string \ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e307f9f2f..8110e2917 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -246,6 +246,49 @@ def list_operation_for_response_test(): return cool_operation +@pytest.fixture +def get_operation_for_subtable_test(): + """ + Creates the following CLI operation: + + GET http://localhost/foo/bar + + Returns { + "table": [ + { + "foo": "", + "bar": 0 + } + ], + "foo": { + "single_nested": { + "foo": "", + "bar": "" + }, + "table": [ + { + "foobar": ["127.0.0.1"] + } + ] + }, + "foobar": "" + } + """ + + spec = _get_parsed_spec("subtable_test_get.yaml") + + dict_values = list(spec.paths.values()) + + # Get parameters for OpenAPIOperation() from yaml fixture + path = dict_values[0] + + command = path.extensions.get("linode-cli-command", "default") + operation = getattr(path, "get") + method = "get" + + return make_test_operation(command, operation, method, path.parameters) + + @pytest.fixture def mocked_config(): """ diff --git a/tests/unit/test_output.py b/tests/unit/test_output.py index fad26ca94..a9b2fe382 100644 --- a/tests/unit/test_output.py +++ b/tests/unit/test_output.py @@ -1,4 +1,5 @@ import io +import json from rich import box from rich import print as rprint @@ -109,6 +110,8 @@ def test_table_output_models( self, mock_cli, list_operation_for_output_tests ): output = io.StringIO() + + title = "cool table" header = ["h1"] data = [ { @@ -121,14 +124,20 @@ def test_table_output_models( columns = [attr] mock_cli.output_handler._table_output( - header, data, columns, "cool table", output + header, data, columns, title, output ) mock_table = io.StringIO() - tab = Table("h1", header_style="", box=box.SQUARE, title_justify="left") + tab = Table( + "h1", + header_style="", + box=box.SQUARE, + title_justify="left", + title=title, + min_width=len(title), + ) for row in [["foo"], ["bar"]]: tab.add_row(*row) - tab.title = "cool table" rprint(tab, file=mock_table) assert output.getvalue() == mock_table.getvalue() @@ -139,6 +148,8 @@ def test_table_output_models_no_headers( mock_cli.output_handler.headers = False output = io.StringIO() + + title = "cool table" header = ["h1"] data = [ { @@ -158,6 +169,8 @@ def test_table_output_models_no_headers( show_header=False, box=box.SQUARE, title_justify="left", + title=title, + min_width=len(title), ) for row in [["foo"], ["bar"]]: tab.add_row(*row) @@ -170,6 +183,8 @@ def test_ascii_table_output( self, mock_cli, list_operation_for_output_tests ): output = io.StringIO() + + title = "cool table" header = ["h1"] data = [ { @@ -181,20 +196,19 @@ def test_ascii_table_output( output_handler = mock_cli.output_handler output_handler._table_output( - header, data, columns, "cool table", output, box.ASCII + header, data, columns, title, output, box.ASCII ) print(output.getvalue()) assert ( - output.getvalue() == "cool \n" - "table \n" - "+-----+\n" - "| h1 |\n" - "|-----|\n" - "| foo |\n" - "| bar |\n" - "+-----+\n" + output.getvalue() == "cool table\n" + "+--------+\n" + "| h1 |\n" + "|--------|\n" + "| foo |\n" + "| bar |\n" + "+--------+\n" ) def test_get_columns_from_model( @@ -242,6 +256,24 @@ def test_get_columns_from_model_select( assert result[0].name == "cool" assert result[1].name == "bar" + # Let's test a single print case + + def test_print_raw(self, mock_cli): + output = io.StringIO() + + mock_cli.output_handler.mode = OutputMode.json + + mock_cli.output_handler.print( + [{"cool": "blah", "bar": "blah2", "test": "blah3"}], + ["cool", "bar", "test"], + to=output, + ) + + assert ( + '[{"cool": "blah", "bar": "blah2", "test": "blah3"}]' + in output.getvalue() + ) + # Let's test a single print case def test_print_response(self, mock_cli, list_operation_for_output_tests): output = io.StringIO() @@ -266,6 +298,7 @@ def test_truncated_table(self, mock_cli, list_operation_for_output_tests): output = io.StringIO() + title = "cool table" test_str = "x" * 80 test_str_truncated = "x…" @@ -278,15 +311,20 @@ def test_truncated_table(self, mock_cli, list_operation_for_output_tests): columns = [list_operation_for_output_tests.response_model.attrs[0]] mock_cli.output_handler._table_output( - header, data, columns, "cool table", output + header, data, columns, title, output ) data[0]["cool"] = test_str_truncated mock_table = io.StringIO() - tab = Table("h1", header_style="", box=box.SQUARE, title_justify="left") + tab = Table( + "h1", + header_style="", + box=box.SQUARE, + title_justify="left", + title=title, + ) tab.add_row(test_str_truncated) - tab.title = "cool table" rprint(tab, file=mock_table) assert output.getvalue() == mock_table.getvalue() @@ -322,6 +360,80 @@ def test_nontruncated_table( tab.title = "cool table" rprint(tab, file=mock_table) - print(output.getvalue()) - assert output.getvalue() != mock_table.getvalue() + + def test_print_subtable(self, mock_cli, get_operation_for_subtable_test): + output = io.StringIO() + + mock_cli.output_handler.mode = OutputMode.table + + mock_data = { + "table": [{"foo": "cool", "bar": 12345}], + "foo": { + "single_nested": {"foo": "cool", "bar": "cool2"}, + "table": [{"foobar": ["127.0.0.1", "127.0.0.2"]}], + }, + "foobar": "wow", + } + + mock_cli.output_handler.print_response( + get_operation_for_subtable_test.response_model, + data=[mock_data], + to=output, + ) + + output = output.getvalue().splitlines() + + lines = [ + "┌────────┐", + "│ foobar │", + "├────────┤", + "│ wow │", + "└────────┘", + "table", + "┌──────┬───────┐", + "│ foo │ bar │", + "├──────┼───────┤", + "│ cool │ 12345 │", + "└──────┴───────┘", + "foo.table", + "┌──────────────────────┐", + "│ foobar │", + "├──────────────────────┤", + "│ 127.0.0.1, 127.0.0.2 │", + "└──────────────────────┘", + "foo.single_nested", + "┌───────┬───────┐", + "│ foo │ bar │", + "├───────┼───────┤", + "│ cool │ cool2 │", + "└───────┴───────┘", + ] + + for i, line in enumerate(lines): + assert line in output[i] + + def test_print_subtable_json( + self, mock_cli, get_operation_for_subtable_test + ): + output = io.StringIO() + + mock_cli.output_handler.mode = OutputMode.json + + mock_data = { + "table": [{"foo": "cool", "bar": 12345}], + "foo": { + "single_nested": {"foo": "cool", "bar": "cool2"}, + "table": [{"foobar": ["127.0.0.1", "127.0.0.2"]}], + }, + "foobar": "wow", + } + + mock_cli.output_handler.print_response( + get_operation_for_subtable_test.response_model, + data=[mock_data], + to=output, + ) + + output = json.loads(output.getvalue()) + assert output == [mock_data]