diff --git a/.circleci/config.yml b/.circleci/config.yml index fe4c371c..658f75d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,6 +24,8 @@ commands: name: Run Maven command: | mvn -s .circleci/mvn-settings.xml << parameters.maven_subcommand >> + - store_artifacts: + path: target/site/jacoco - run: name: Save test results command: | diff --git a/.gitignore b/.gitignore index 39736a21..b65cb041 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +.DS_Store diff --git a/pom.xml b/pom.xml index 59ce218d..292d8a30 100644 --- a/pom.xml +++ b/pom.xml @@ -53,12 +53,12 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220719.154219-3 com.kingsrook.qqq qqq-backend-module-rdbms - 0.2.0-SNAPSHOT + 0.2.0-20220719.155012-5 test @@ -97,6 +97,12 @@ 5.8.1 test + + org.assertj + assertj-core + 3.23.1 + test + diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java index c949c9fd..970034b4 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -90,11 +90,13 @@ public class QCommandBuilder // add table-specific sub-commands for the table // /////////////////////////////////////////////////// tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table)); - tableCommand.addSubcommand("count", defineQueryCommand(table)); + tableCommand.addSubcommand("count", defineCountCommand(table)); + tableCommand.addSubcommand("get", defineGetCommand(table)); tableCommand.addSubcommand("query", defineQueryCommand(table)); tableCommand.addSubcommand("insert", defineInsertCommand(table)); tableCommand.addSubcommand("update", defineUpdateCommand(table)); tableCommand.addSubcommand("delete", defineDeleteCommand(table)); + tableCommand.addSubcommand("export", defineExportCommand(table)); List processes = qInstance.getProcessesForTable(tableName); if(CollectionUtils.nullSafeHasContents(processes)) @@ -158,6 +160,71 @@ public class QCommandBuilder + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineExportCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec exportCommand = CommandLine.Model.CommandSpec.create(); + exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-f", "--filename") + .type(String.class) + .description("File name (including path) to write to. File extension will be used to determine the report format. Supported formats are: csv, xlsx.") + .required(true) + .build()); + exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-e", "--fieldNames") + .type(String.class) + .description("Comma-separated list of field names (e.g., from table meta-data) to include in the export. If not given, then all fields in the table are included.") + .build()); + exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-l", "--limit") + .type(int.class) + .description("Optional limit on the max number of records to include in the export.") + .build()); + exportCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") + .type(String[].class) + .description("Query filter criteria for the export. May be given multiple times. Use format: $fieldName $operator $value. e.g., id EQUALS 42") + .build()); + + // todo - add the fields as explicit params? + + return exportCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineGetCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec getCommand = CommandLine.Model.CommandSpec.create(); + getCommand.addPositional(CommandLine.Model.PositionalParamSpec.builder() + .index("0") + // .type(String.class) // todo - mmm, better as picocli's "compound" thing, w/ the actual pkey's type? + .description("Primary key value from the table") + .build()); + + return getCommand; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private CommandLine.Model.CommandSpec defineCountCommand(QTableMetaData table) + { + CommandLine.Model.CommandSpec countCommand = CommandLine.Model.CommandSpec.create(); + countCommand.addOption(CommandLine.Model.OptionSpec.builder("-c", "--criteria") + .type(String[].class) + .build()); + + // todo - add the fields as explicit params? + + return countCommand; + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index 8177b6df..e34de75d 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -23,7 +23,9 @@ package com.kingsrook.qqq.frontend.picocli; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.Serializable; @@ -35,6 +37,7 @@ import java.util.Map; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -46,12 +49,16 @@ import com.kingsrook.qqq.backend.core.adapters.JsonToQRecordAdapter; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; @@ -75,7 +82,9 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.core.config.Configurator; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.OptionSpec; @@ -94,10 +103,10 @@ import picocli.CommandLine.UnmatchedArgumentException; *******************************************************************************/ public class QPicoCliImplementation { - public static final int DEFAULT_LIMIT = 20; + public static final int DEFAULT_QUERY_LIMIT = 20; private static QInstance qInstance; - private static QSession session; + private static QSession session; @@ -112,14 +121,14 @@ public class QPicoCliImplementation // parse args to look up metaData and prime instance if(args.length > 0 && args[0].startsWith("--qInstanceJsonFile=")) { - String filePath = args[0].replaceFirst("--.*=", ""); + String filePath = args[0].replaceFirst("--.*=", ""); String qInstanceJson = FileUtils.readFileToString(new File(filePath)); qInstance = new QInstanceAdapter().jsonToQInstanceIncludingBackends(qInstanceJson); String[] subArgs = Arrays.copyOfRange(args, 1, args.length); QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); - int exitCode = qPicoCliImplementation.runCli("qapi", subArgs); + int exitCode = qPicoCliImplementation.runCli("qapi", subArgs); System.exit(exitCode); } else @@ -136,6 +145,14 @@ public class QPicoCliImplementation *******************************************************************************/ public QPicoCliImplementation(QInstance qInstance) { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // use the qqq-picocli log4j config, less the system property log4j.configurationFile was set by the runner // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(System.getProperty("log4j.configurationFile") == null) + { + Configurator.initialize(null, "qqq-picocli-log4j2.xml"); + } + QPicoCliImplementation.qInstance = qInstance; } @@ -232,7 +249,7 @@ public class QPicoCliImplementation private static void setupSession(String[] args) throws QModuleDispatchException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); // todo - does this need some per-provider logic actually? mmm... Map authenticationContext = new HashMap<>(); @@ -254,7 +271,7 @@ public class QPicoCliImplementation else { ParseResult subParseResult = parseResult.subcommand(); - String subCommandName = subParseResult.commandSpec().name(); + String subCommandName = subParseResult.commandSpec().name(); CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName); switch(subCommandName) { @@ -285,7 +302,7 @@ public class QPicoCliImplementation if(tableParseResult.hasSubcommand()) { ParseResult subParseResult = tableParseResult.subcommand(); - String subCommandName = subParseResult.commandSpec().name(); + String subCommandName = subParseResult.commandSpec().name(); switch(subCommandName) { case "meta-data": @@ -296,10 +313,19 @@ public class QPicoCliImplementation { return runTableCount(commandLine, tableName, subParseResult); } + case "get": + { + CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName); + return runTableGet(commandLine, tableName, subParseResult, subCommandLine); + } case "query": { return runTableQuery(commandLine, tableName, subParseResult); } + case "export": + { + return runTableExport(commandLine, tableName, subParseResult); + } case "insert": { return runTableInsert(commandLine, tableName, subParseResult); @@ -352,7 +378,7 @@ public class QPicoCliImplementation /////////////////////////////////////////// // move on to running the actual process // /////////////////////////////////////////// - String subCommandName = subParseResult.subcommand().commandSpec().name(); + String subCommandName = subParseResult.subcommand().commandSpec().name(); CommandLine subCommandLine = commandLine.getSubcommands().get(subCommandName); return runActualProcess(subCommandLine, subParseResult.subcommand()); } @@ -365,9 +391,9 @@ public class QPicoCliImplementation *******************************************************************************/ private int runActualProcess(CommandLine subCommandLine, ParseResult processParseResult) { - String processName = processParseResult.commandSpec().name(); - QProcessMetaData process = qInstance.getProcess(processName); - RunProcessInput request = new RunProcessInput(qInstance); + String processName = processParseResult.commandSpec().name(); + QProcessMetaData process = qInstance.getProcess(processName); + RunProcessInput request = new RunProcessInput(qInstance); request.setSession(session); request.setProcessName(processName); @@ -443,6 +469,49 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private int runTableGet(CommandLine commandLine, String tableName, ParseResult subParseResult, CommandLine subCommandLine) throws QException + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(tableName); + queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); + queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); + String primaryKeyValue = subParseResult.matchedPositionalValue(0, null); + + if(primaryKeyValue == null) + { + subCommandLine.usage(commandLine.getOut()); + return commandLine.getCommandSpec().exitCodeOnUsageHelp(); + } + + QTableMetaData table = queryInput.getTable(); + QQueryFilter filter = new QQueryFilter() + .withCriteria(new QFilterCriteria() + .withFieldName(table.getPrimaryKeyField()) + .withOperator(QCriteriaOperator.EQUALS) + .withValues(List.of(primaryKeyValue))); + queryInput.setFilter(filter); + + QueryAction queryAction = new QueryAction(); + QueryOutput queryOutput = queryAction.execute(queryInput); + List records = queryOutput.getRecords(); + if(records.isEmpty()) + { + commandLine.getOut().println("No " + table.getLabel() + " found for " + table.getField(table.getPrimaryKeyField()).getLabel() + ": " + primaryKeyValue); + return commandLine.getCommandSpec().exitCodeOnInvalidInput(); + } + else + { + commandLine.getOut().println(JsonUtils.toPrettyJson(records.get(0))); + return commandLine.getCommandSpec().exitCodeOnSuccess(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -452,7 +521,7 @@ public class QPicoCliImplementation queryInput.setSession(session); queryInput.setTableName(tableName); queryInput.setSkip(subParseResult.matchedOptionValue("skip", null)); - queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT)); + queryInput.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_QUERY_LIMIT)); queryInput.setFilter(generateQueryFilter(subParseResult)); QueryAction queryAction = new QueryAction(); @@ -463,6 +532,77 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private int runTableExport(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException + { + String filename = subParseResult.matchedOptionValue("--filename", ""); + + ///////////////////////////////////////////////////////////////////////////////////////// + // if a format query param wasn't given, then try to get file extension from file name // + ///////////////////////////////////////////////////////////////////////////////////////// + ReportFormat reportFormat; + if(filename.contains(".")) + { + reportFormat = ReportFormat.fromString(filename.substring(filename.lastIndexOf(".") + 1)); + } + else + { + throw (new QUserFacingException("File name did not contain an extension, so report format could not be inferred.")); + } + + OutputStream outputStream; + try + { + outputStream = new FileOutputStream(filename); + } + catch(Exception e) + { + throw (new QException("Error opening report file: " + e.getMessage(), e)); + } + + try + { + ///////////////////////////////////////////// + // set up the report action's input object // + ///////////////////////////////////////////// + ReportInput reportInput = new ReportInput(qInstance); + reportInput.setSession(session); + reportInput.setTableName(tableName); + reportInput.setReportFormat(reportFormat); + reportInput.setFilename(filename); + reportInput.setReportOutputStream(outputStream); + reportInput.setLimit(subParseResult.matchedOptionValue("limit", null)); + + reportInput.setQueryFilter(generateQueryFilter(subParseResult)); + + String fieldNames = subParseResult.matchedOptionValue("--fieldNames", ""); + if(StringUtils.hasContent(fieldNames)) + { + reportInput.setFieldNames(Arrays.asList(fieldNames.split(","))); + } + + ReportOutput reportOutput = new ReportAction().execute(reportInput); + + commandLine.getOut().println("Wrote " + reportOutput.getRecordCount() + " records to file " + filename); + return commandLine.getCommandSpec().exitCodeOnSuccess(); + } + finally + { + try + { + outputStream.close(); + } + catch(IOException e) + { + throw (new QException("Error closing report file", e)); + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -474,7 +614,7 @@ public class QPicoCliImplementation for(String criterion : criteria) { // todo - parse! - String[] parts = criterion.split(" "); + String[] parts = criterion.split(" "); QFilterCriteria qQueryCriteria = new QFilterCriteria(); qQueryCriteria.setFieldName(parts[0]); qQueryCriteria.setOperator(QCriteriaOperator.valueOf(parts[1])); @@ -532,7 +672,7 @@ public class QPicoCliImplementation try { String path = subParseResult.matchedOptionValue("--csvFile", ""); - String csv = FileUtils.readFileToString(new File(path)); + String csv = FileUtils.readFileToString(new File(path)); recordList = new CsvToQRecordAdapter().buildRecordsFromCsv(csv, table, mapping); } catch(IOException e) @@ -589,7 +729,7 @@ public class QPicoCliImplementation boolean anyFields = false; - String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); + String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); Serializable[] primaryKeyValues = primaryKeyOption.split(","); for(Serializable primaryKeyValue : primaryKeyValues) { @@ -638,7 +778,7 @@ public class QPicoCliImplementation ///////////////////////////////////////////// // get the pKeys that the user specified // ///////////////////////////////////////////// - String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); + String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); Serializable[] primaryKeyValues = primaryKeyOption.split(","); deleteInput.setPrimaryKeys(Arrays.asList(primaryKeyValues)); diff --git a/src/main/resources/qqq-picocli-log4j2.xml b/src/main/resources/qqq-picocli-log4j2.xml new file mode 100644 index 00000000..5b03a388 --- /dev/null +++ b/src/main/resources/qqq-picocli-log4j2.xml @@ -0,0 +1,18 @@ + + + + %date{ISO8601} | %relative | %level | %threadName{1} | %logger{1}.%method | %message%n + + + + + + + + + + + + + + diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java index 8e14abd8..9f0f53fb 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -31,6 +31,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.UUID; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -40,6 +41,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -226,7 +228,7 @@ class QPicoCliImplementationTest int count = countResult.getInt("count"); assertEquals(4, count); - testOutput = testCli("person", "count", "--criteria", "id EQUALS 3"); + testOutput = testCli("person", "count", "--criteria", "id EQUALS 3"); countResult = JsonUtils.toJSONObject(testOutput.getOutput()); assertNotNull(countResult); count = countResult.getInt("count"); @@ -254,6 +256,49 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** test running a "get single record" action (singleton query) on a table + ** + *******************************************************************************/ + @Test + public void test_tableGetNoIdGiven() + { + TestOutput testOutput = testCli("person", "get"); + assertTestOutputContains(testOutput, "Usage: " + CLI_NAME + " person get PARAM"); + assertTestOutputContains(testOutput, "Primary key value from the table"); + } + + + + /******************************************************************************* + ** test running a "get single record" action (singleton query) on a table + ** + *******************************************************************************/ + @Test + public void test_tableGet() + { + TestOutput testOutput = testCli("person", "get", "1"); + JSONObject getResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(getResult); + assertEquals(1, getResult.getJSONObject("values").getInt("id")); + assertEquals("Darin", getResult.getJSONObject("values").getString("firstName")); + } + + + + /******************************************************************************* + ** test running a "get single record" action (singleton query) on a table + ** + *******************************************************************************/ + @Test + public void test_tableGetMissingId() + { + TestOutput testOutput = testCli("person", "get", "1976"); + assertTestOutputContains(testOutput, "No Person found for Id: 1976"); + } + + + /******************************************************************************* ** test running an insert w/o specifying any fields, prints usage ** @@ -422,6 +467,9 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** + *******************************************************************************/ private void assertRowValueById(String tableName, String columnName, String value, Integer id) throws Exception { TestUtils.runTestSql("SELECT " + columnName + " FROM " + tableName + " WHERE id=" + id, (rs -> { @@ -470,7 +518,7 @@ class QPicoCliImplementationTest ** *******************************************************************************/ @Test - public void test_tableProcess() throws Exception + public void test_tableProcess() { TestOutput testOutput = testCli("person", "process"); @@ -487,7 +535,7 @@ class QPicoCliImplementationTest ** *******************************************************************************/ @Test - public void test_tableProcessUnknownName() throws Exception + public void test_tableProcessUnknownName() { String badProcessName = "not-a-process"; TestOutput testOutput = testCli("person", "process", badProcessName); @@ -502,7 +550,7 @@ class QPicoCliImplementationTest ** *******************************************************************************/ @Test - public void test_tableProcessGreetUsingCallbackForFields() throws Exception + public void test_tableProcessGreetUsingCallbackForFields() { setStandardInputLines("Hi", "How are you?"); TestOutput testOutput = testCli("person", "process", "greet"); @@ -511,12 +559,216 @@ class QPicoCliImplementationTest } + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportNoArgsExcel() + { + String filename = "/tmp/" + UUID.randomUUID() + ".xlsx"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename); + assertTestOutputContains(testOutput, "Wrote 5 records to file " + filename); + + File file = new File(filename); + assertTrue(file.exists()); + + // todo - some day when we learn to read Excel, assert that we wrote as expected. + + deleteFile(file); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportWithLimit() throws Exception + { + String filename = "/tmp/" + UUID.randomUUID() + ".csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--limit=3"); + assertTestOutputContains(testOutput, "Wrote 3 records to file " + filename); + + File file = new File(filename); + @SuppressWarnings("unchecked") + List list = FileUtils.readLines(file); + assertEquals(4, list.size()); + assertThat(list.get(0)).contains(""" + "Id","Create Date","Modify Date\""""); + assertThat(list.get(1)).matches(""" + ^"1",.*"Darin.*"""); + assertThat(list.get(3)).matches(""" + ^"3",.*"Tim.*"""); + + deleteFile(file); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportWithCriteria() throws Exception + { + String filename = "/tmp/" + UUID.randomUUID() + ".csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--criteria", "id NOT_EQUALS 3"); + assertTestOutputContains(testOutput, "Wrote 4 records to file " + filename); + + File file = new File(filename); + @SuppressWarnings("unchecked") + List list = FileUtils.readLines(file); + assertEquals(5, list.size()); + assertThat(list.get(0)).contains(""" + "Id","Create Date","Modify Date\""""); + assertThat(list.get(1)).matches("^\"1\",.*"); + assertThat(list.get(2)).matches("^\"2\",.*"); + assertThat(list.get(3)).matches("^\"4\",.*"); + assertThat(list.get(4)).matches("^\"5\",.*"); + + deleteFile(file); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportWithoutFilename() + { + TestOutput testOutput = testCli("person", "export"); + assertTestErrorContains(testOutput, "Missing required option: '--filename=PARAM'"); + assertTestErrorContains(testOutput, "Usage: " + CLI_NAME + " person export"); + assertTestErrorContains(testOutput, "-f=PARAM"); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportNoFileExtension() + { + String filename = "/tmp/" + UUID.randomUUID(); + TestOutput testOutput = testCli("person", "export", "--filename=" + filename); + assertTestErrorContains(testOutput, "File name did not contain an extension"); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportBadFileType() + { + String filename = "/tmp/" + UUID.randomUUID() + ".docx"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename); + assertTestErrorContains(testOutput, "Unsupported report format: docx."); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportBadFilePath() + { + String filename = "/no-such/directory/" + UUID.randomUUID() + "report.csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename); + assertTestErrorContains(testOutput, "No such file or directory"); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportBadFieldNams() + { + String filename = "/tmp/" + UUID.randomUUID() + ".csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo"); + assertTestErrorContains(testOutput, "Field name foo was not found on the Person table"); + } + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportBadFieldNames() + { + String filename = "/tmp/" + UUID.randomUUID() + ".csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=foo,bar,baz"); + assertTestErrorContains(testOutput, "Fields names foo, bar, and baz were not found on the Person table"); + } + + + + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportGoodFieldNamesXslx() throws IOException + { + String filename = "/tmp/" + UUID.randomUUID() + ".xlsx"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate"); + + File file = new File(filename); + assertTrue(file.exists()); + + // todo - some day when we learn to read Excel, assert that we wrote as expected (with 3 columns) + + deleteFile(file); + } + + /******************************************************************************* + ** test exporting a table + ** + *******************************************************************************/ + @Test + public void test_tableExportGoodFieldNamesCSV() throws IOException + { + String filename = "/tmp/" + UUID.randomUUID() + ".csv"; + TestOutput testOutput = testCli("person", "export", "--filename=" + filename, "--fieldNames=id,lastName,birthDate"); + + File file = new File(filename); + @SuppressWarnings("unchecked") + List list = FileUtils.readLines(file); + assertEquals(6, list.size()); + assertThat(list.get(0)).isEqualTo(""" + "Id","Last Name","Birth Date\""""); + assertThat(list.get(1)).isEqualTo(""" + "1","Kelkhoff","1980-05-31\""""); + + deleteFile(file); + } + + + /******************************************************************************* ** test running a process on a table ** *******************************************************************************/ @Test - public void test_tableProcessGreetUsingOptionsForFields() throws Exception + public void test_tableProcessGreetUsingOptionsForFields() { TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=World"); assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field"); @@ -567,6 +819,16 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** delete a file, asserting that we did so. + *******************************************************************************/ + private void deleteFile(File file) + { + assertTrue(file.delete()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -628,118 +890,4 @@ class QPicoCliImplementationTest System.setIn(stdin); } - - - /******************************************************************************* - ** - *******************************************************************************/ - private static class TestOutput - { - private String output; - private String[] outputLines; - private String error; - private String[] errorLines; - - - - /******************************************************************************* - ** - *******************************************************************************/ - public TestOutput(String output, String error) - { - this.output = output; - this.error = error; - - this.outputLines = output.split("\n"); - this.errorLines = error.split("\n"); - } - - - - /******************************************************************************* - ** Getter for output - ** - *******************************************************************************/ - public String getOutput() - { - return output; - } - - - - /******************************************************************************* - ** Setter for output - ** - *******************************************************************************/ - public void setOutput(String output) - { - this.output = output; - } - - - - /******************************************************************************* - ** Getter for outputLines - ** - *******************************************************************************/ - public String[] getOutputLines() - { - return outputLines; - } - - - - /******************************************************************************* - ** Setter for outputLines - ** - *******************************************************************************/ - public void setOutputLines(String[] outputLines) - { - this.outputLines = outputLines; - } - - - - /******************************************************************************* - ** Getter for error - ** - *******************************************************************************/ - public String getError() - { - return error; - } - - - - /******************************************************************************* - ** Setter for error - ** - *******************************************************************************/ - public void setError(String error) - { - this.error = error; - } - - - - /******************************************************************************* - ** Getter for errorLines - ** - *******************************************************************************/ - public String[] getErrorLines() - { - return errorLines; - } - - - - /******************************************************************************* - ** Setter for errorLines - ** - *******************************************************************************/ - public void setErrorLines(String[] errorLines) - { - this.errorLines = errorLines; - } - } } diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java new file mode 100644 index 00000000..fcbb76d8 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestOutput.java @@ -0,0 +1,136 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.frontend.picocli; + + +/******************************************************************************* + ** + *******************************************************************************/ +class TestOutput +{ + private String output; + private String[] outputLines; + private String error; + private String[] errorLines; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public TestOutput(String output, String error) + { + this.output = output; + this.error = error; + + this.outputLines = output.split("\n"); + this.errorLines = error.split("\n"); + } + + + + /******************************************************************************* + ** Getter for output + ** + *******************************************************************************/ + public String getOutput() + { + return output; + } + + + + /******************************************************************************* + ** Setter for output + ** + *******************************************************************************/ + public void setOutput(String output) + { + this.output = output; + } + + + + /******************************************************************************* + ** Getter for outputLines + ** + *******************************************************************************/ + public String[] getOutputLines() + { + return outputLines; + } + + + + /******************************************************************************* + ** Setter for outputLines + ** + *******************************************************************************/ + public void setOutputLines(String[] outputLines) + { + this.outputLines = outputLines; + } + + + + /******************************************************************************* + ** Getter for error + ** + *******************************************************************************/ + public String getError() + { + return error; + } + + + + /******************************************************************************* + ** Setter for error + ** + *******************************************************************************/ + public void setError(String error) + { + this.error = error; + } + + + + /******************************************************************************* + ** Getter for errorLines + ** + *******************************************************************************/ + public String[] getErrorLines() + { + return errorLines; + } + + + + /******************************************************************************* + ** Setter for errorLines + ** + *******************************************************************************/ + public void setErrorLines(String[] errorLines) + { + this.errorLines = errorLines; + } +}