This is an automated email from the ASF dual-hosted git repository. jmclean pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push: new 539ac3634 [#5961] feat(CLI): Add details and list command to CLI for model. (#6053) 539ac3634 is described below commit 539ac3634bd142d63e8878408dd858ca7ccb5216 Author: Lord of Abyss <103809695+abyss-l...@users.noreply.github.com> AuthorDate: Thu Jan 2 07:02:10 2025 +0800 [#5961] feat(CLI): Add details and list command to CLI for model. (#6053) ### What changes were proposed in this pull request? add get and list command to model. - get: `gcli model get -m demo_metalake --name catalog.schema`, Displays the name of the Model along with all versions. - list:`gcli model list -m demo_metalake --name catalog.schema`, Display all models in this schema. ### Why are the changes needed? Fix: #5961 ### Does this PR introduce _any_ user-facing change? NO ### How was this patch tested? ut --- .../org/apache/gravitino/cli/CommandEntities.java | 2 + .../org/apache/gravitino/cli/ErrorMessages.java | 1 + .../java/org/apache/gravitino/cli/FullName.java | 9 + .../apache/gravitino/cli/GravitinoCommandLine.java | 39 +++ .../apache/gravitino/cli/TestableCommandLine.java | 12 + .../apache/gravitino/cli/commands/ListModel.java | 80 ++++++ .../gravitino/cli/commands/ModelDetails.java | 92 +++++++ .../org/apache/gravitino/cli/TestModelCommand.java | 270 +++++++++++++++++++++ 8 files changed, 505 insertions(+) diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java b/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java index dc0332029..2dd50974e 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java @@ -37,6 +37,7 @@ public class CommandEntities { public static final String TOPIC = "topic"; public static final String FILESET = "fileset"; public static final String ROLE = "role"; + public static final String MODEL = "model"; private static final HashSet<String> VALID_ENTITIES = new HashSet<>(); @@ -52,6 +53,7 @@ public class CommandEntities { VALID_ENTITIES.add(TOPIC); VALID_ENTITIES.add(FILESET); VALID_ENTITIES.add(ROLE); + VALID_ENTITIES.add(MODEL); } /** diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java index 757e7c2cb..4bd523ec2 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java @@ -28,6 +28,7 @@ public class ErrorMessages { public static final String UNKNOWN_CATALOG = "Unknown catalog name."; public static final String UNKNOWN_SCHEMA = "Unknown schema name."; public static final String UNKNOWN_TABLE = "Unknown table name."; + public static final String UNKNOWN_MODEL = "Unknown model name."; public static final String MALFORMED_NAME = "Malformed entity name."; public static final String MISSING_NAME = "Missing --name option."; public static final String MISSING_METALAKE = "Missing --metalake option."; diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java b/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java index 8af7322dc..c21d21af4 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java @@ -96,6 +96,15 @@ public class FullName { return getNamePart(1); } + /** + * Retrieves the model name from the second part of the full name option. + * + * @return The model name, or null if not found + */ + public String getModelName() { + return getNamePart(2); + } + /** * Retrieves the table name from the third part of the full name option. * diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java index 7869dd97b..8cd335beb 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java @@ -152,6 +152,8 @@ public class GravitinoCommandLine extends TestableCommandLine { handleTagCommand(); } else if (entity.equals(CommandEntities.ROLE)) { handleRoleCommand(); + } else if (entity.equals(CommandEntities.MODEL)) { + handleModelCommand(); } } @@ -1150,6 +1152,43 @@ public class GravitinoCommandLine extends TestableCommandLine { } } + private void handleModelCommand() { + String url = getUrl(); + String auth = getAuth(); + String userName = line.getOptionValue(GravitinoOptions.LOGIN); + FullName name = new FullName(line); + String metalake = name.getMetalakeName(); + String catalog = name.getCatalogName(); + String schema = name.getSchemaName(); + + Command.setAuthenticationMode(auth, userName); + + List<String> missingEntities = Lists.newArrayList(); + if (catalog == null) missingEntities.add(CommandEntities.CATALOG); + if (schema == null) missingEntities.add(CommandEntities.SCHEMA); + + // Handle CommandActions.LIST action separately as it doesn't require the `model` + if (CommandActions.LIST.equals(command)) { + checkEntities(missingEntities); + newListModel(url, ignore, metalake, catalog, schema).handle(); + return; + } + + String model = name.getModelName(); + if (model == null) missingEntities.add(CommandEntities.MODEL); + checkEntities(missingEntities); + + switch (command) { + case CommandActions.DETAILS: + newModelDetails(url, ignore, metalake, catalog, schema, model).handle(); + break; + + default: + System.err.println(ErrorMessages.UNSUPPORTED_ACTION); + break; + } + } + /** * Retrieves the Gravitinno URL from the command line options or the GRAVITINO_URL environment * variable or the Gravitio config file. diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java index f07244c00..3cfd84ad8 100644 --- a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java @@ -66,6 +66,7 @@ import org.apache.gravitino.cli.commands.ListGroups; import org.apache.gravitino.cli.commands.ListIndexes; import org.apache.gravitino.cli.commands.ListMetalakeProperties; import org.apache.gravitino.cli.commands.ListMetalakes; +import org.apache.gravitino.cli.commands.ListModel; import org.apache.gravitino.cli.commands.ListRoles; import org.apache.gravitino.cli.commands.ListSchema; import org.apache.gravitino.cli.commands.ListSchemaProperties; @@ -79,6 +80,7 @@ import org.apache.gravitino.cli.commands.MetalakeAudit; import org.apache.gravitino.cli.commands.MetalakeDetails; import org.apache.gravitino.cli.commands.MetalakeDisable; import org.apache.gravitino.cli.commands.MetalakeEnable; +import org.apache.gravitino.cli.commands.ModelDetails; import org.apache.gravitino.cli.commands.OwnerDetails; import org.apache.gravitino.cli.commands.RemoveAllTags; import org.apache.gravitino.cli.commands.RemoveCatalogProperty; @@ -907,4 +909,14 @@ public class TestableCommandLine { String url, boolean ignore, String metalake, String catalog) { return new CatalogDisable(url, ignore, metalake, catalog); } + + protected ListModel newListModel( + String url, boolean ignore, String metalake, String catalog, String schema) { + return new ListModel(url, ignore, metalake, catalog, schema); + } + + protected ModelDetails newModelDetails( + String url, boolean ignore, String metalake, String catalog, String schema, String model) { + return new ModelDetails(url, ignore, metalake, catalog, schema, model); + } } diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java new file mode 100644 index 000000000..1528e954b --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListModel.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.cli.commands; + +import com.google.common.base.Joiner; +import java.util.Arrays; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Namespace; +import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.client.GravitinoClient; +import org.apache.gravitino.exceptions.NoSuchCatalogException; +import org.apache.gravitino.exceptions.NoSuchMetalakeException; +import org.apache.gravitino.exceptions.NoSuchSchemaException; + +/** List the names of all models in a schema. */ +public class ListModel extends Command { + protected final String metalake; + protected final String catalog; + protected final String schema; + + /** + * List the names of all models in a schema. + * + * @param url The URL of the Gravitino server. + * @param ignoreVersions If true don't check the client/server versions match. + * @param metalake The name of the metalake. + * @param catalog The name of the catalog. + * @param schema The name of schema. + */ + public ListModel( + String url, boolean ignoreVersions, String metalake, String catalog, String schema) { + super(url, ignoreVersions); + this.metalake = metalake; + this.catalog = catalog; + this.schema = schema; + } + + /** List the names of all models in a schema. */ + @Override + public void handle() { + NameIdentifier[] models = new NameIdentifier[0]; + Namespace name = Namespace.of(schema); + + try { + GravitinoClient client = buildClient(metalake); + models = client.loadCatalog(catalog).asModelCatalog().listModels(name); + } catch (NoSuchMetalakeException noSuchMetalakeException) { + exitWithError(ErrorMessages.UNKNOWN_METALAKE); + } catch (NoSuchCatalogException noSuchCatalogException) { + exitWithError(ErrorMessages.UNKNOWN_CATALOG); + } catch (NoSuchSchemaException noSuchSchemaException) { + exitWithError(ErrorMessages.UNKNOWN_SCHEMA); + } catch (Exception err) { + exitWithError(err.getMessage()); + } + + String output = + models.length == 0 + ? "No models exist." + : Joiner.on(",").join(Arrays.stream(models).map(model -> model.name()).iterator()); + + System.out.println(output); + } +} diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java new file mode 100644 index 000000000..6c3aec08f --- /dev/null +++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ModelDetails.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.cli.commands; + +import java.util.Arrays; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.cli.ErrorMessages; +import org.apache.gravitino.client.GravitinoClient; +import org.apache.gravitino.exceptions.NoSuchCatalogException; +import org.apache.gravitino.exceptions.NoSuchMetalakeException; +import org.apache.gravitino.exceptions.NoSuchModelException; +import org.apache.gravitino.exceptions.NoSuchSchemaException; +import org.apache.gravitino.model.Model; +import org.apache.gravitino.model.ModelCatalog; + +/** Displays the details of a model. */ +public class ModelDetails extends Command { + protected final String metalake; + protected final String catalog; + protected final String schema; + protected final String model; + + /** + * Displays the details of a model. + * + * @param url The URL of the Gravitino server. + * @param ignoreVersions If true don't check the client/server versions match. + * @param metalake The name of the metalake. + * @param catalog The name of the catalog. + * @param schema The name of schema. + * @param model The name of model. + */ + public ModelDetails( + String url, + boolean ignoreVersions, + String metalake, + String catalog, + String schema, + String model) { + super(url, ignoreVersions); + this.metalake = metalake; + this.catalog = catalog; + this.schema = schema; + this.model = model; + } + + /** Displays the details of a model. */ + @Override + public void handle() { + NameIdentifier name = NameIdentifier.of(schema, model); + Model gModel = null; + int[] versions = new int[0]; + + try { + GravitinoClient client = buildClient(metalake); + ModelCatalog modelCatalog = client.loadCatalog(catalog).asModelCatalog(); + gModel = modelCatalog.getModel(name); + versions = modelCatalog.listModelVersions(name); + } catch (NoSuchMetalakeException noSuchMetalakeException) { + exitWithError(ErrorMessages.UNKNOWN_METALAKE); + } catch (NoSuchCatalogException noSuchCatalogException) { + exitWithError(ErrorMessages.UNKNOWN_CATALOG); + } catch (NoSuchSchemaException noSuchSchemaException) { + exitWithError(ErrorMessages.UNKNOWN_SCHEMA); + } catch (NoSuchModelException noSuchModelException) { + exitWithError(ErrorMessages.UNKNOWN_MODEL); + } catch (Exception err) { + exitWithError(err.getMessage()); + } + String basicInfo = + String.format("Model name %s, latest version: %s%n", gModel.name(), gModel.latestVersion()); + String versionInfo = Arrays.toString(versions); + System.out.printf(basicInfo + "versions: " + versionInfo); + } +} diff --git a/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommand.java b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommand.java new file mode 100644 index 000000000..d222655b6 --- /dev/null +++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestModelCommand.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.gravitino.cli; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.gravitino.cli.commands.ListModel; +import org.apache.gravitino.cli.commands.ModelDetails; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.com.google.common.base.Joiner; + +public class TestModelCommand { + private final Joiner joiner = Joiner.on(", ").skipNulls(); + private CommandLine mockCommandLine; + private Options mockOptions; + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @BeforeEach + void setUp() { + mockCommandLine = mock(CommandLine.class); + mockOptions = mock(Options.class); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void testListModelCommand() { + ListModel mockList = mock(ListModel.class); + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema"); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.LIST)); + + doReturn(mockList) + .when(commandLine) + .newListModel( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + eq("catalog"), + eq("schema")); + commandLine.handleCommandLine(); + verify(mockList).handle(); + } + + @Test + void testListModelCommandWithoutCatalog() { + Main.useExit = false; + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(false); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.LIST)); + + assertThrows(RuntimeException.class, commandLine::handleCommandLine); + verify(commandLine, never()) + .newListModel( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + isNull(), + isNull()); + String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); + assertEquals( + ErrorMessages.MISSING_NAME + + "\n" + + "Missing required argument(s): " + + joiner.join(Arrays.asList(CommandEntities.CATALOG, CommandEntities.SCHEMA)), + output); + } + + @Test + void testListModelCommandWithoutSchema() { + Main.useExit = false; + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog"); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.LIST)); + + assertThrows(RuntimeException.class, commandLine::handleCommandLine); + verify(commandLine, never()) + .newListModel( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + eq("catalog"), + isNull()); + String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); + assertEquals( + ErrorMessages.MALFORMED_NAME + + "\n" + + "Missing required argument(s): " + + joiner.join(Collections.singletonList(CommandEntities.SCHEMA)), + output); + } + + @Test + void testModelDetailsCommand() { + ModelDetails mockList = mock(ModelDetails.class); + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema.model"); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.DETAILS)); + + doReturn(mockList) + .when(commandLine) + .newModelDetails( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + eq("catalog"), + eq("schema"), + eq("model")); + commandLine.handleCommandLine(); + verify(mockList).handle(); + } + + @Test + void testModelDetailsCommandWithoutCatalog() { + Main.useExit = false; + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(false); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.DETAILS)); + + assertThrows(RuntimeException.class, commandLine::handleCommandLine); + + verify(commandLine, never()) + .newModelDetails( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + isNull(), + isNull(), + isNull()); + String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); + assertEquals( + ErrorMessages.MISSING_NAME + + "\n" + + "Missing required argument(s): " + + joiner.join( + Arrays.asList( + CommandEntities.CATALOG, CommandEntities.SCHEMA, CommandEntities.MODEL)), + output); + } + + @Test + void testModelDetailsCommandWithoutSchema() { + Main.useExit = false; + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog"); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.DETAILS)); + + assertThrows(RuntimeException.class, commandLine::handleCommandLine); + + verify(commandLine, never()) + .newModelDetails( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + eq("catalog"), + isNull(), + isNull()); + String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); + assertEquals( + ErrorMessages.MALFORMED_NAME + + "\n" + + "Missing required argument(s): " + + joiner.join(Arrays.asList(CommandEntities.SCHEMA, CommandEntities.MODEL)), + output); + } + + @Test + void testModelDetailsCommandWithoutModel() { + Main.useExit = false; + when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo"); + when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true); + when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog.schema"); + GravitinoCommandLine commandLine = + spy( + new GravitinoCommandLine( + mockCommandLine, mockOptions, CommandEntities.MODEL, CommandActions.DETAILS)); + + assertThrows(RuntimeException.class, commandLine::handleCommandLine); + + verify(commandLine, never()) + .newModelDetails( + eq(GravitinoCommandLine.DEFAULT_URL), + eq(false), + eq("metalake_demo"), + eq("catalog"), + eq("schema"), + isNull()); + String output = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim(); + assertEquals( + ErrorMessages.MALFORMED_NAME + + "\n" + + "Missing required argument(s): " + + joiner.join(Collections.singletonList(CommandEntities.MODEL)), + output); + } +}