diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ef02745..fe4c371c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ jobs: executor: java17 steps: - run_maven: - maven_subcommand: test + maven_subcommand: verify - slack/notify: event: fail diff --git a/checkstyle.xml b/checkstyle.xml index 76f872ed..f5e7412d 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -181,8 +181,8 @@ --> - + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + pre-unit-test + + prepare-agent + + + jaCoCoArgLine + + + + unit-test-check + + check + + + + ${coverage.haltOnFailure} + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${coverage.instructionCoveredRatioMinimum} + + + + + + + + post-unit-test + verify + + report + + + + + + exec-maven-plugin + org.codehaus.mojo + 3.0.0 + + + test-coverage-summary + verify + + exec + + + sh + + -c + + /tmp/$$.headers +xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values +echo +echo "Jacoco coverage summary report:" +echo " See also target/site/jacoco/index.html" +echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html" +echo "------------------------------------------------------------" +paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}' +rm /tmp/$$.headers /tmp/$$.values + ]]> + + + + + + 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 9d2525df..d2fbed7b 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -90,6 +90,7 @@ public class QCommandBuilder // add table-specific sub-commands for the table // /////////////////////////////////////////////////// tableCommand.addSubcommand("meta-data", defineMetaDataCommand(table)); + tableCommand.addSubcommand("count", defineQueryCommand(table)); tableCommand.addSubcommand("query", defineQueryCommand(table)); tableCommand.addSubcommand("insert", defineInsertCommand(table)); tableCommand.addSubcommand("update", defineUpdateCommand(table)); 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 1302cb94..56cd335a 100644 --- a/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.CountAction; import com.kingsrook.qqq.backend.core.actions.DeleteAction; import com.kingsrook.qqq.backend.core.actions.InsertAction; import com.kingsrook.qqq.backend.core.actions.MetaDataAction; @@ -45,6 +46,8 @@ 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.model.actions.count.CountRequest; +import com.kingsrook.qqq.backend.core.model.actions.count.CountResult; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest; import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult; import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest; @@ -94,7 +97,7 @@ public class QPicoCliImplementation public static final int DEFAULT_LIMIT = 20; private static QInstance qInstance; - private static QSession session; + private static QSession session; @@ -109,14 +112,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 @@ -229,7 +232,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<>(); @@ -251,7 +254,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) { @@ -282,13 +285,17 @@ 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": { return runTableMetaData(commandLine, tableName, subParseResult); } + case "count": + { + return runTableCount(commandLine, tableName, subParseResult); + } case "query": { return runTableQuery(commandLine, tableName, subParseResult); @@ -345,7 +352,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()); } @@ -358,9 +365,9 @@ public class QPicoCliImplementation *******************************************************************************/ private int runActualProcess(CommandLine subCommandLine, ParseResult processParseResult) { - String processName = processParseResult.commandSpec().name(); - QProcessMetaData process = qInstance.getProcess(processName); - RunProcessRequest request = new RunProcessRequest(qInstance); + String processName = processParseResult.commandSpec().name(); + QProcessMetaData process = qInstance.getProcess(processName); + RunProcessRequest request = new RunProcessRequest(qInstance); request.setSession(session); request.setProcessName(processName); @@ -384,9 +391,10 @@ public class QPicoCliImplementation subCommandLine.getOut().format(" %s: %s\n", outputField.getLabel(), result.getValues().get(outputField.getName())); } - if(result.getError() != null) + if(result.getException().isPresent()) { - subCommandLine.getOut().println("Process Error message: " + result.getError()); + // todo - user-facing, similar to javalin + subCommandLine.getOut().println("Process Error message: " + result.getException().get().getMessage()); } } catch(Exception e) @@ -417,6 +425,24 @@ public class QPicoCliImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private int runTableCount(CommandLine commandLine, String tableName, ParseResult subParseResult) throws QException + { + CountRequest countRequest = new CountRequest(qInstance); + countRequest.setSession(session); + countRequest.setTableName(tableName); + countRequest.setFilter(generateQueryFilter(subParseResult)); + + CountAction countAction = new CountAction(); + CountResult countResult = countAction.execute(countRequest); + commandLine.getOut().println(JsonUtils.toPrettyJson(countResult)); + return commandLine.getCommandSpec().exitCodeOnSuccess(); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -427,15 +453,28 @@ public class QPicoCliImplementation queryRequest.setTableName(tableName); queryRequest.setSkip(subParseResult.matchedOptionValue("skip", null)); queryRequest.setLimit(subParseResult.matchedOptionValue("limit", DEFAULT_LIMIT)); + queryRequest.setFilter(generateQueryFilter(subParseResult)); + QueryAction queryAction = new QueryAction(); + QueryResult queryResult = queryAction.execute(queryRequest); + commandLine.getOut().println(JsonUtils.toPrettyJson(queryResult)); + return commandLine.getCommandSpec().exitCodeOnSuccess(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QQueryFilter generateQueryFilter(ParseResult subParseResult) + { QQueryFilter filter = new QQueryFilter(); - queryRequest.setFilter(filter); String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {}); 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])); @@ -443,10 +482,7 @@ public class QPicoCliImplementation filter.addCriteria(qQueryCriteria); } - QueryAction queryAction = new QueryAction(); - QueryResult queryResult = queryAction.execute(queryRequest); - commandLine.getOut().println(JsonUtils.toPrettyJson(queryResult)); - return commandLine.getCommandSpec().exitCodeOnSuccess(); + return filter; } @@ -496,7 +532,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) @@ -553,7 +589,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) { @@ -602,7 +638,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(","); deleteRequest.setPrimaryKeys(Arrays.asList(primaryKeyValues)); 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 4f298522..8e14abd8 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementationTest.java @@ -202,7 +202,6 @@ class QPicoCliImplementationTest assertNotNull(metaData); assertEquals(1, metaData.keySet().size(), "Number of top-level keys"); JSONObject table = metaData.getJSONObject("table"); - assertEquals(4, table.keySet().size(), "Number of mid-level keys"); assertEquals("person", table.getString("name")); assertEquals("Person", table.getString("label")); assertEquals("id", table.getString("primaryKeyField")); @@ -214,6 +213,28 @@ class QPicoCliImplementationTest + /******************************************************************************* + ** test running a count on a table + ** + *******************************************************************************/ + @Test + public void test_tableCount() + { + TestOutput testOutput = testCli("person", "count", "--criteria", "id NOT_EQUALS 3"); + JSONObject countResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(countResult); + int count = countResult.getInt("count"); + assertEquals(4, count); + + testOutput = testCli("person", "count", "--criteria", "id EQUALS 3"); + countResult = JsonUtils.toJSONObject(testOutput.getOutput()); + assertNotNull(countResult); + count = countResult.getInt("count"); + assertEquals(1, count); + } + + + /******************************************************************************* ** test running a query on a table ** @@ -497,9 +518,9 @@ class QPicoCliImplementationTest @Test public void test_tableProcessGreetUsingOptionsForFields() throws Exception { - TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=There"); + TestOutput testOutput = testCli("person", "process", "greet", "--field-greetingPrefix=Hello", "--field-greetingSuffix=World"); assertTestOutputDoesNotContain(testOutput, "Please supply a value for the field"); - assertTestOutputContains(testOutput, "Hello X There"); + assertTestOutputContains(testOutput, "Hello X World"); } diff --git a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index 7940bbcc..c72c140b 100644 --- a/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -25,7 +25,7 @@ package com.kingsrook.qqq.frontend.picocli; import java.io.InputStream; import java.sql.Connection; import java.util.List; -import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody; +import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.QCodeType; @@ -34,16 +34,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView; -import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.apache.commons.io.IOUtils; import static junit.framework.Assert.assertNotNull; @@ -166,12 +164,12 @@ public class TestUtils return new QProcessMetaData() .withName("greet") .withTableName("person") - .addFunction(new QFunctionMetaData() + .addStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() - .withName(MockFunctionBody.class.getName()) + .withName(MockBackendStep.class.getName()) .withCodeType(QCodeType.JAVA) - .withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context? + .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) .withFieldList(List.of( @@ -184,9 +182,6 @@ public class TestUtils .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) - .withOutputView(new QOutputView() - .withMessageField("outputMessage") - .withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting")))) ); }