Merged dev into feature/CE-876-develop-missing-widget-types

This commit is contained in:
2024-02-20 20:12:55 -06:00
35 changed files with 2822 additions and 89 deletions

View File

@ -0,0 +1,220 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunnerTest.runAllTableActions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/*******************************************************************************
** Test for the case where:
** - inserting into a main table and a child table, and the child table has a
** post-insert customizer, which mo
*******************************************************************************/
public class PollingAutomationPerTableRunnerAutomtationUpdatingSelfAvoidInfiniteLoopTest extends BaseTest
{
private static boolean didFailInThread = false;
static
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// we can set this property to revert to the behavior that existed before this test was written. //
///////////////////////////////////////////////////////////////////////////////////////////////////
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach()
{
didFailInThread = false;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
////////////////////////////////////
// add automations to order table //
////////////////////////////////////
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER)
.withField(TestUtils.standardQqqAutomationStatusField())
.withAutomationDetails(TestUtils.defineStandardAutomationDetails()
.withAction(new TableAutomationAction()
.withName("orderPostInsertAction")
.withTriggerEvent(TriggerEvent.POST_INSERT)
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class)))
.withAction(new TableAutomationAction()
.withName("orderPostUpdateAction")
.withTriggerEvent(TriggerEvent.POST_UPDATE)
.withCodeReference(new QCodeReference(OrderPostInsertAndUpdateAction.class))));
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord().withValue("orderNo", "10101").withValue("total", new BigDecimal(1))));
new InsertAction().execute(insertInput);
//////////////////////////////////////////////////////
// make sure the order is in pending-inserts status //
//////////////////////////////////////////////////////
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(1), order.getValueBigDecimal("total"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
// run automations - that should update the order via the automation - but leave status as OK //
////////////////////////////////////////////////////////////////////////////////////////////////
runAllTableActions(QContext.getQInstance());
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
}
//////////////////////////////////////////////////////////////////
// now update the order, verify status moves to pending-updates //
//////////////////////////////////////////////////////////////////
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
.withValue("id", 1)
.withValue("storeId", "x")));
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
assertEquals("x", order.getValueString("storeId"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
// run automations - that should update the order via the automation - but leave status as OK //
////////////////////////////////////////////////////////////////////////////////////////////////
runAllTableActions(QContext.getQInstance());
assertFalse(didFailInThread, "A failure condition happened in the automation sub-thread. Check System.out for message.");
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(3), order.getValueBigDecimal("total"));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class OrderPostInsertAndUpdateAction extends RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// launch a new thread, to make sure we avoid the "stack contains automations" check in RecordAutomationStatusUpdater //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
CapturedContext capturedContext = QContext.capture();
for(QRecord record : recordAutomationInput.getRecordList())
{
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
Future<?> submit = service.submit(() ->
{
QContext.init(capturedContext);
try
{
new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_ORDER).withRecord(new QRecord()
.withValue("id", record.getValue("id"))
.withValue("total", record.getValueBigDecimal("total").add(new BigDecimal(1)))
));
///////////////////////////////////////////////////////////////////
// make sure that update action didn't change the order's status //
///////////////////////////////////////////////////////////////////
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
if(Objects.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName())))
{
System.out.println("Failing test - expected status to not be [PENDING_UPDATE_AUTOMATIONS], but it was.");
didFailInThread = true;
}
assertNotEquals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
}
catch(QException e)
{
e.printStackTrace();
}
finally
{
QContext.clear();
}
});
while(!submit.isDone())
{
}
}
}
}
}

View File

@ -0,0 +1,224 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunnerTest.runAllTableActions;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Test for the case where:
** - inserting into a main table and a child table, and the child table has a
** post-insert customizer, which mo
*******************************************************************************/
public class PollingAutomationPerTableRunnerChildPostInsertCustomizerTest extends BaseTest
{
static
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// we can set this property to revert to the behavior that existed before this test was written. //
///////////////////////////////////////////////////////////////////////////////////////////////////
// System.setProperty("qqq.recordAutomationStatusUpdater.skipPreUpdateFetch", "true");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
////////////////////////////////////
// add automations to order table //
////////////////////////////////////
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_ORDER)
.withField(TestUtils.standardQqqAutomationStatusField())
.withAutomationDetails(TestUtils.defineStandardAutomationDetails()
.withAction(new TableAutomationAction()
.withName("orderPostInsertAction")
.withTriggerEvent(TriggerEvent.POST_INSERT)
.withCodeReference(new QCodeReference(OrderPostInsertAction.class))
));
///////////////////////////////////////////////////////////////////////////
// add a post-insert customizer to line-ite table (child of order table) //
///////////////////////////////////////////////////////////////////////////
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_LINE_ITEM)
.withCustomizer(TableCustomizers.POST_INSERT_RECORD, new QCodeReference(LineItemPostInsertCustomizer.class));
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(new QRecord()
.withValue("orderNo", "10101")
.withAssociatedRecord("orderLine", new QRecord()
.withValue("sku", "ABC")
.withValue("quantity", 1))));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure the order is in pending-inserts status (at one time, a bug meant that it wouldn't have been...) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(1), order.getValueBigDecimal("total"));
}
///////////////////////////////////////////////////////////////////////////////////////////////
// run automations - that should... insert a second line item, but should leave the order in //
// automation-status = OK, to avoid perpetual re-running //
// the line-item post-inserter should run a second time, making the order's total = 2 //
///////////////////////////////////////////////////////////////////////////////////////////////
runAllTableActions(QContext.getQInstance());
{
QRecord order = new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_ORDER).withPrimaryKey(1));
assertEquals(AutomationStatus.OK.getId(), order.getValue(TestUtils.standardQqqAutomationStatusField().getName()));
assertEquals(new BigDecimal(2), order.getValueBigDecimal("total"));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class OrderPostInsertAction extends RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
///////////////////////////////////////
// add a new line item to the orders //
///////////////////////////////////////
List<QRecord> lineItemsToInsert = new ArrayList<>();
for(QRecord record : recordAutomationInput.getRecordList())
{
lineItemsToInsert.add(new QRecord()
.withValue("orderId", record.getValue("id"))
.withValue("sku", UUID.randomUUID())
.withValue("quantity", 1)
);
}
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_LINE_ITEM).withRecords(lineItemsToInsert));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static class LineItemPostInsertCustomizer extends AbstractPostInsertCustomizer
{
@Override
public List<QRecord> apply(List<QRecord> records) throws QException
{
//////////////////////////////////
// count line items by order id //
//////////////////////////////////
Set<Serializable> orderIds = records.stream().map(r -> r.getValue("orderId")).collect(Collectors.toSet());
GroupBy groupByOrderId = new GroupBy(QFieldType.STRING, "orderId");
Aggregate countId = new Aggregate("id", AggregateOperator.COUNT);
AggregateInput aggregateInput = new AggregateInput();
aggregateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
aggregateInput.setFilter(new QQueryFilter(new QFilterCriteria("orderId", QCriteriaOperator.IN, orderIds)));
aggregateInput.withGroupBy(groupByOrderId);
aggregateInput.withAggregate(countId);
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
Map<Integer, Integer> countByOrderId = new HashMap<>();
for(AggregateResult result : aggregateOutput.getResults())
{
countByOrderId.put(ValueUtils.getValueAsInteger(result.getGroupByValue(groupByOrderId)), ValueUtils.getValueAsInteger(result.getAggregateValue(countId)));
}
///////////////////////////////////
// update the order total fields //
// s/b in bulk, but, meh //
///////////////////////////////////
for(Integer orderId : countByOrderId.keySet())
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(TestUtils.TABLE_NAME_ORDER);
updateInput.setRecords(List.of(new QRecord()
.withValue("id", orderId)
.withValue("total", new BigDecimal(countByOrderId.get(orderId)))));
new UpdateAction().execute(updateInput);
}
return (records);
}
}
}

View File

@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -56,14 +57,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAut
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcessTest;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -77,18 +75,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
class PollingAutomationPerTableRunnerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
@AfterEach
void beforeAndAfterEach()
{
MemoryRecordStore.getInstance().reset();
}
/*******************************************************************************
** Test a cycle that does an insert, some automations, then and an update, and more automations.
*******************************************************************************/
@ -200,7 +186,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private void runAllTableActions(QInstance qInstance) throws QException
static void runAllTableActions(QInstance qInstance) throws QException
{
List<PollingAutomationPerTableRunner.TableActionsInterface> tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, TestUtils.POLLING_AUTOMATION);
for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
@ -210,7 +196,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
/////////////////////////////////////////////////////////////////////////////////////////////////////
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status());
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
}
}
@ -512,7 +498,7 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
/////////////////////////////////////////////////////////////////////////////////////////////////////
// note - don't call run - it is meant to be called async - e.g., it sets & clears thread context. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), QContext.getQSession(), tableAction.status());
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
}
}).hasMessage(PollingAutomationPerTableRunnerThatShouldSimulateServerShutdownMidRun.EXCEPTION_MESSAGE);
@ -593,4 +579,70 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
new PollingAutomationPerTableRunner.ShardedTableActions(null, null, null, null, null).noopToFakeTestCoverage();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAddOrderByToQueryFilter()
{
//////////////////////////////////////////////////////////////////////////
// make a table we'll test with. just put a primary-key id on it first //
//////////////////////////////////////////////////////////////////////////
QTableMetaData table = new QTableMetaData()
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER));
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
}
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
}
////////////////////////////////////////////////////////////////////////////////
// add createDate & modifyDate fields, but not with dynamic-default-behaviors //
// so should still sort by id //
////////////////////////////////////////////////////////////////////////////////
QFieldMetaData createDate = new QFieldMetaData("createDate", QFieldType.DATE_TIME);
QFieldMetaData modifyDate = new QFieldMetaData("modifyDate", QFieldType.DATE_TIME);
table.addField(createDate);
table.addField(modifyDate);
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
}
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
assertEquals("id", filter.getOrderBys().get(0).getFieldName());
}
/////////////////////////////////////////////////////////////////////////////////////
// add dynamic default value behaviors, confirm create/modify date fields are used //
/////////////////////////////////////////////////////////////////////////////////////
createDate.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE);
modifyDate.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter);
assertEquals("createDate", filter.getOrderBys().get(0).getFieldName());
}
{
QQueryFilter filter = new QQueryFilter();
PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter);
assertEquals("modifyDate", filter.getOrderBys().get(0).getFieldName());
}
}
}

View File

@ -0,0 +1,58 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for AbstractHTMLWidgetRenderer
*******************************************************************************/
class AbstractHTMLWidgetRendererTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
String link = AbstractHTMLWidgetRenderer.getCountLink(null, TestUtils.TABLE_NAME_PERSON, new QQueryFilter()
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1))
.withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), 2
);
////////////////////////////////////////////////////
// assert that filter de-duplication is occurring //
////////////////////////////////////////////////////
assertThat(link).doesNotMatch(".*EQUALS.*EQUALS.*");
}
}

View File

@ -26,6 +26,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
@ -175,7 +176,7 @@ class QRecordTest extends BaseTest
**
*******************************************************************************/
@Test
void testListAsValue()
void testArrayListAsValue()
{
ArrayList<Integer> originalArrayList = new ArrayList<>(List.of(1, 2, 3));
QRecord recordWithArrayListValue = new QRecord().withValue("myList", originalArrayList);
@ -196,6 +197,31 @@ class QRecordTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLinkedListAsValue()
{
LinkedList<Integer> originalLinkedList = new LinkedList<>(List.of(1, 2, 3));
QRecord recordWithLinkedListValue = new QRecord().withValue("myList", originalLinkedList);
QRecord cloneWithLinkedListValue = new QRecord(recordWithLinkedListValue);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the clone list and original list should be equals (have contents that are equals), but not be the same (reference) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertEquals(List.of(1, 2, 3), cloneWithLinkedListValue.getValue("myList"));
assertNotSame(originalLinkedList, cloneWithLinkedListValue.getValue("myList"));
//////////////////////////////////////////////////////////////////////////////////////////////////////
// make sure a change to the original list doesn't change the cloned list (as it was cloned deeply) //
//////////////////////////////////////////////////////////////////////////////////////////////////////
originalLinkedList.add(4);
assertNotEquals(originalLinkedList, cloneWithLinkedListValue.getValue("myList"));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,204 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for HealBadRecordAutomationStatusesProcessStep
*******************************************************************************/
class HealBadRecordAutomationStatusesProcessStepTest extends BaseTest
{
private static String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY;
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTwoFailedUpdates() throws QException
{
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
List<QRecord> records = queryAllRecords();
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
RunBackendStepOutput output = runProcessStep();
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords()).allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneFailedUpdateOneFailedInsert() throws QException
{
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(new QRecord(), new QRecord())));
List<QRecord> records = queryAllRecords();
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(0, 1), AutomationStatus.FAILED_UPDATE_AUTOMATIONS, null);
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records.subList(1, 2), AutomationStatus.FAILED_INSERT_AUTOMATIONS, null);
assertThat(queryAllRecords())
.anyMatch(r -> AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
.anyMatch(r -> AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
RunBackendStepOutput output = runProcessStep();
assertEquals(2, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords())
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
.anyMatch(r -> AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOldRunning() throws QException
{
/////////////////////////////////////////////////
// temporarily remove the modify-date behavior //
/////////////////////////////////////////////////
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.NONE);
//////////////////////////////////////////////////////////////////////////
// insert 2 records, one with an old modifyDate, one with 6 minutes ago //
//////////////////////////////////////////////////////////////////////////
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
new QRecord().withValue("firstName", "Darin").withValue("modifyDate", Instant.parse("2023-01-01T12:00:00Z")),
new QRecord().withValue("firstName", "Tim").withValue("modifyDate", Instant.now().minus(6, ChronoUnit.MINUTES))
)));
List<QRecord> records = queryAllRecords();
///////////////////////////////////////////////////////
// put those records both in status: running-updates //
///////////////////////////////////////////////////////
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(QContext.getQInstance().getTable(tableName), records, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS, null);
assertThat(queryAllRecords())
.allMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
/////////////////////////////////////
// restore the modifyDate behavior //
/////////////////////////////////////
QContext.getQInstance().getTable(tableName).getField("modifyDate").withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
/////////////////////////
// run code under test //
/////////////////////////
RunBackendStepOutput output = runProcessStep();
/////////////////////////////////////////////////////////////////////////////////////////////
// assert we updated 1 (the old one) to pending-updates, the other left as running-updates //
/////////////////////////////////////////////////////////////////////////////////////////////
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords())
.anyMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)))
.anyMatch(r -> AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
/////////////////////////////////
// re-run, with 3-minute limit //
/////////////////////////////////
output = runProcessStep(new RunBackendStepInput().withValues(Map.of("minutesOldLimit", 3)));
/////////////////////////////////////////////////////////////////
// assert that one updated too, and all are now pending-update //
/////////////////////////////////////////////////////////////////
assertEquals(1, output.getValueInteger("totalRecordsUpdated"));
assertThat(queryAllRecords())
.allMatch(r -> AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(getAutomationStatus(r)));
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getAutomationStatus(QRecord r)
{
return r.getValueInteger(TestUtils.standardQqqAutomationStatusField().getName());
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> queryAllRecords() throws QException
{
return new QueryAction().execute(new QueryInput(tableName)).getRecords();
}
/*******************************************************************************
**
*******************************************************************************/
private static RunBackendStepOutput runProcessStep() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
return runProcessStep(input);
}
/*******************************************************************************
**
*******************************************************************************/
private static RunBackendStepOutput runProcessStep(RunBackendStepInput input) throws QException
{
RunBackendStepOutput output = new RunBackendStepOutput();
new HealBadRecordAutomationStatusesProcessStep().run(input, output);
return output;
}
}

View File

@ -0,0 +1,97 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.automation;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for RunTableAutomationsProcessStep
*******************************************************************************/
class RunTableAutomationsProcessStepTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws Exception
{
UnsafeSupplier<Integer, ?> getAutomationStatus = () -> new GetAction().executeForRecord(new GetInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withPrimaryKey(1)).getValueInteger("qqqAutomationStatus");
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
assertEquals(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(), getAutomationStatus.get());
RunBackendStepInput input = new RunBackendStepInput();
input.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
RunBackendStepOutput output = new RunBackendStepOutput();
new RunTableAutomationsProcessStep().run(input, output);
assertEquals("true", output.getValue("ok"));
assertEquals(AutomationStatus.OK.getId(), getAutomationStatus.get());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testThrowsWithoutTableName() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
.hasMessageContaining("Missing required input value: tableName");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testThrowsWithInvalidTableName() throws QException
{
RunBackendStepInput input = new RunBackendStepInput();
RunBackendStepOutput output = new RunBackendStepOutput();
input.addValue("tableName", "asdf");
assertThatThrownBy(() -> new RunTableAutomationsProcessStep().run(input, output))
.hasMessageContaining("Unrecognized table name: asdf");
}
}

View File

@ -116,6 +116,19 @@ class ExceptionUtilsTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetTopAndBottomMessages()
{
assertEquals("foo", ExceptionUtils.getTopAndBottomMessages(new Exception("foo")));
assertEquals("foo: bar", ExceptionUtils.getTopAndBottomMessages(new Exception("foo", new Exception("bar"))));
assertEquals("foo: baz", ExceptionUtils.getTopAndBottomMessages(new Exception("foo", new Exception("bar", new Exception("baz")))));
}
/*******************************************************************************
** Test exception class - lets you set the cause, easier to create a loop.
*******************************************************************************/

View File

@ -0,0 +1,355 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR;
import static com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper.dedupeFilter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for QQueryFilterDeduper
*******************************************************************************/
class QQueryFilterDeduperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testDegenerateCases()
{
assertNull(dedupeFilter(null));
QQueryFilter empty = new QQueryFilter();
assertEquals(empty, dedupeFilter(empty));
assertNotSame(empty, dedupeFilter(empty)); // method always clones, so, just assert that.
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSimpleFiltersWithNoChanges()
{
QQueryFilter oneCriteria = new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1));
assertEquals(oneCriteria, dedupeFilter(oneCriteria));
assertNotSame(oneCriteria, dedupeFilter(oneCriteria));
QQueryFilter twoCriteriaDifferentFields = new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("b", GREATER_THAN, 2));
assertEquals(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
assertNotSame(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOrs()
{
///////////////////////////////////////////////////////
// we've only written the simplest cases with ORs... //
///////////////////////////////////////////////////////
assertEquals(new QQueryFilter().withBooleanOperator(OR).withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withBooleanOperator(OR)
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
));
//////////////////////////////////////////////////////////////////////
// just not built at this time - obviously, could become an IN list //
//////////////////////////////////////////////////////////////////////
QQueryFilter notSupportedOrTwoEquals = new QQueryFilter()
.withBooleanOperator(OR)
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", EQUALS, 2));
assertEquals(notSupportedOrTwoEquals, dedupeFilter(notSupportedOrTwoEquals));
///////////////////////////////////////////////////////////////////////////////////
// I think the logic would be, that the EQUALS 1 would be removed (is redundant) //
///////////////////////////////////////////////////////////////////////////////////
QQueryFilter notSupportedOrEqualsNotEquals = new QQueryFilter()
.withBooleanOperator(OR)
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2));
assertEquals(notSupportedOrEqualsNotEquals, dedupeFilter(notSupportedOrEqualsNotEquals));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMoreOperators()
{
//////////////////////////////////////////////////////////////////////
// only simplest case (of criteria being .equals()) is supported... //
//////////////////////////////////////////////////////////////////////
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
.withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
));
///////////////////////////////////////////////////////////////////////////////////
// in theory, we could do more, but we just haven't yet (e.g, this could be > 5) //
///////////////////////////////////////////////////////////////////////////////////
QQueryFilter tooComplex = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", GREATER_THAN, 1))
.withCriteria(new QFilterCriteria("f", GREATER_THAN, 5));
assertEquals(tooComplex, dedupeFilter(tooComplex));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAllEquals()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
));
assertEquals(new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
.withCriteria(new QFilterCriteria("c", EQUALS, 3)),
dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
.withCriteria(new QFilterCriteria("b", EQUALS, 2))
.withCriteria(new QFilterCriteria("a", EQUALS, 1))
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
.withCriteria(new QFilterCriteria("c", EQUALS, 3))
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testEqualsAndNotEqualsAndNotIn()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
));
////////////////////////////////////////////////////////////
// this is a contradiction, so we choose not to dedupe it //
////////////////////////////////////////////////////////////
QQueryFilter contradiction1 = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
assertEquals(contradiction1, dedupeFilter(contradiction1));
QQueryFilter contradiction2 = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_IN, 0, 1));
assertEquals(contradiction2, dedupeFilter(contradiction2));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this case can collapse the two not-equals, but then fails to merge the equals with them, because they are a contradiction! //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertEquals(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
.withCriteria(new QFilterCriteria("f", EQUALS, 2)),
dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
.withCriteria(new QFilterCriteria("f", EQUALS, 2))
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNotEqualsAndNotIn()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
));
//////////////////////////////////////////////////////////////////////////////////////////
// ideally, maybe, this would have the values ordered 1,2,3, but, is equivalent enough //
//////////////////////////////////////////////////////////////////////////////////////////
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2))
.withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInAndNotEquals()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
));
QQueryFilter contradiction1 = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
.withCriteria(new QFilterCriteria("f", IN, 1));
assertEquals(contradiction1, dedupeFilter(contradiction1));
QQueryFilter contradiction2 = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1))
.withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
assertEquals(contradiction2, dedupeFilter(contradiction2));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMultipleInLists()
{
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
.withCriteria(new QFilterCriteria("f", IN, 2, 3))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3, 4)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
.withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
));
assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3)), dedupeFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
.withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
.withCriteria(new QFilterCriteria("f", IN, 1, 3, 5, 7))
));
///////////////////////////////////////////////////////////////////
// contradicting in-lists - we give up and refuse to simplify it //
///////////////////////////////////////////////////////////////////
QQueryFilter contradiction = new QQueryFilter()
.withCriteria(new QFilterCriteria("f", IN, 1, 2))
.withCriteria(new QFilterCriteria("f", IN, 3, 4));
assertEquals(contradiction, dedupeFilter(contradiction));
}
}

View File

@ -720,7 +720,7 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static QTableAutomationDetails defineStandardAutomationDetails()
public static QTableAutomationDetails defineStandardAutomationDetails()
{
return (new QTableAutomationDetails()
.withProviderName(POLLING_AUTOMATION)

View File

@ -32,11 +32,14 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import org.junit.jupiter.api.Disabled;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -122,6 +125,58 @@ class MemoizationTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLookupFunction()
{
AtomicInteger lookupFunctionCallCounter = new AtomicInteger(0);
Memoization<String, Integer> memoization = new Memoization<>();
UnsafeFunction<String, Integer, Exception> lookupFunction = numberString ->
{
lookupFunctionCallCounter.getAndIncrement();
if(numberString.equals("null"))
{
return (null);
}
return Integer.parseInt(numberString);
};
//////////////////////////////////////////////////////////////////////////////////////////
// get "1" twice - should return 1 each time, and call the lookup function exactly once //
//////////////////////////////////////////////////////////////////////////////////////////
assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1);
assertEquals(1, lookupFunctionCallCounter.get());
assertThat(memoization.getResult("1", lookupFunction)).isPresent().contains(1);
assertEquals(1, lookupFunctionCallCounter.get());
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// now get "null" twice - should return null each time, and call the lookup function exactly once more //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
assertThat(memoization.getResult("null", lookupFunction)).isEmpty();
assertEquals(2, lookupFunctionCallCounter.get());
assertThat(memoization.getResult("null", lookupFunction)).isEmpty();
assertEquals(2, lookupFunctionCallCounter.get());
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// now make a call that throws twice - again, should return null each time, and only do one more loookup call //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertThat(memoization.getResult(null, lookupFunction)).isEmpty();
assertEquals(3, lookupFunctionCallCounter.get());
assertThat(memoization.getResult(null, lookupFunction)).isEmpty();
assertEquals(3, lookupFunctionCallCounter.get());
}
/*******************************************************************************
**
*******************************************************************************/