package jnpf.elnService.impl; import jnpf.elnEntity.formula.ElnFormulaParameterSlotEntity; import jnpf.elnEntity.formula.ElnFormulaVersionEntity; import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity; import jnpf.elnEntity.runtime.ElnFormulaCalculationLogEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnMapper.ElnFormulaCalculationLogMapper; import jnpf.elnMapper.ElnFormulaParameterSlotMapper; import jnpf.elnMapper.ElnFormulaVersionMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnMapper.ElnTemplateFormulaBindingMapper; import jnpf.elnService.formula.FormulaDependencyGraph; import jnpf.elnService.formula.impl.FormulaPreviewServiceImpl; import jnpf.elnService.formula.impl.FormulaValidationServiceImpl; import org.junit.Test; import java.lang.reflect.Proxy; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class ElnRecordFormulaCalculationServiceImplTest { @Test public void recalculatesOnlyAffectedFormulaClosureInTopologicalOrder() { Fixture fixture = new Fixture(false); List calculated = fixture.service.recalculateAffected( "tenant-a", fixture.record, fixture.revision, fixture.values, new LinkedHashSet<>(Collections.singletonList("input-a")), "save-1", "user-1"); assertEquals(Arrays.asList("result-a", "result-b"), bindingIds(calculated)); assertEquals(new BigDecimal("3"), fixture.value("result-a").getZhiShuzhi()); assertEquals(new BigDecimal("4"), fixture.value("result-b").getZhiShuzhi()); assertEquals(new BigDecimal("300"), fixture.value("result-c").getZhiShuzhi()); assertEquals(2, fixture.logs.size()); assertEquals("SUCCESS", fixture.logs.get(0).getJisuanZhuangtai()); assertEquals("SUCCESS", fixture.logs.get(1).getJisuanZhuangtai()); assertEquals(new BigDecimal("2"), fixture.logs.get(0).getShuruKuaizhao().get("x")); assertEquals(new BigDecimal("3"), fixture.logs.get(1).getShuruKuaizhao().get("x")); } @Test public void clearsFailedFormulaAndEveryDownstreamOldResultWithLogs() { Fixture fixture = new Fixture(true); List calculated = fixture.service.recalculateAffected( "tenant-a", fixture.record, fixture.revision, fixture.values, new LinkedHashSet<>(Collections.singletonList("input-a")), "save-2", "user-1"); assertEquals(Arrays.asList("result-a", "result-b"), bindingIds(calculated)); assertNull(fixture.value("result-a").getZhiWenben()); assertNull(fixture.value("result-a").getZhiShuzhi()); assertNull(fixture.value("result-b").getZhiWenben()); assertNull(fixture.value("result-b").getZhiShuzhi()); assertEquals("MISSING_INPUT", fixture.value("result-a").getJisuanZhuangtai()); assertEquals("MISSING_INPUT", fixture.value("result-b").getJisuanZhuangtai()); assertEquals(new BigDecimal("300"), fixture.value("result-c").getZhiShuzhi()); assertEquals(2, fixture.logs.size()); assertEquals("MISSING_INPUT", fixture.logs.get(0).getJisuanZhuangtai()); assertEquals("MISSING_INPUT", fixture.logs.get(1).getJisuanZhuangtai()); } @Test public void recordsDownstreamAsMissingWhenUpstreamFormulaErrors() { Fixture fixture = new Fixture(false); fixture.value("input-a").setZhiShuzhi(null); fixture.value("input-a").setZhiWenben("invalid-number"); fixture.service.recalculateAffected( "tenant-a", fixture.record, fixture.revision, fixture.values, new LinkedHashSet<>(Collections.singletonList("input-a")), "save-3", "user-1"); assertEquals("ERROR", fixture.value("result-a").getJisuanZhuangtai()); assertEquals("MISSING_INPUT", fixture.value("result-b").getJisuanZhuangtai()); assertNull(fixture.value("result-b").getZhiShuzhi()); assertEquals("MISSING_INPUT", fixture.logs.get(1).getJisuanZhuangtai()); assertTrue(fixture.logs.get(1).getShibaiYuanyin() .contains("code-result-a")); } @Test public void recalculatesEveryFormulaInTopologicalOrder() { Fixture fixture = new Fixture(false); List calculated = fixture.service.recalculateAll( "tenant-a", fixture.record, fixture.revision, fixture.values, "all-1", "user-1"); assertEquals(Arrays.asList("result-a", "result-b", "result-c"), bindingIds(calculated)); assertEquals(new BigDecimal("3"), fixture.value("result-a").getZhiShuzhi()); assertEquals(new BigDecimal("4"), fixture.value("result-b").getZhiShuzhi()); assertEquals(new BigDecimal("10"), fixture.value("result-c").getZhiShuzhi()); assertEquals(3, fixture.logs.size()); } private static List bindingIds(List values) { List result = new ArrayList<>(); for (ElnRecordFieldValueEntity value : values) result.add(value.getMubanBangdingId()); return result; } private static final class Fixture { private final ElnRecordEntity record = new ElnRecordEntity(); private final ElnRecordRevisionEntity revision = new ElnRecordRevisionEntity(); private final List values = new ArrayList<>(); private final List logs = new ArrayList<>(); private final ElnRecordFormulaCalculationServiceImpl service; private Fixture(boolean missingInput) { record.setId("record-1"); record.setMubanId("template-1"); revision.setId("revision-1"); revision.setDianziJiluId(record.getId()); List formulas = Arrays.asList( formula("formula-a", "definition-a", "version-a", "input-a", "result-a"), formula("formula-b", "definition-b", "version-b", "result-a", "result-b"), formula("formula-c", "definition-c", "version-c", "input-c", "result-c")); List bindings = Arrays.asList( binding("input-a"), binding("result-a"), binding("result-b"), binding("input-c"), binding("result-c")); values.add(value("input-a", missingInput ? null : new BigDecimal("2"))); values.add(value("result-a", new BigDecimal("100"))); values.add(value("result-b", new BigDecimal("200"))); values.add(value("input-c", new BigDecimal("9"))); values.add(value("result-c", new BigDecimal("300"))); AtomicInteger versionIndex = new AtomicInteger(); AtomicInteger slotIndex = new AtomicInteger(); List versions = Arrays.asList( version("version-a", "definition-a"), version("version-b", "definition-b"), version("version-c", "definition-c")); List> slots = Arrays.asList( Collections.singletonList(slot("version-a")), Collections.singletonList(slot("version-b")), Collections.singletonList(slot("version-c"))); ElnTemplateFormulaBindingMapper formulaMapper = proxy( ElnTemplateFormulaBindingMapper.class, (method, args) -> "selectList".equals(method) ? formulas : null); ElnFormulaVersionMapper versionMapper = proxy(ElnFormulaVersionMapper.class, (method, args) -> "selectOne".equals(method) ? versions.get(versionIndex.getAndIncrement()) : null); ElnFormulaParameterSlotMapper slotMapper = proxy( ElnFormulaParameterSlotMapper.class, (method, args) -> "selectList".equals(method) ? slots.get(slotIndex.getAndIncrement()) : null); ElnTemplateBindingMapper bindingMapper = proxy(ElnTemplateBindingMapper.class, (method, args) -> "selectList".equals(method) ? bindings : null); ElnFormulaCalculationLogMapper logMapper = proxy( ElnFormulaCalculationLogMapper.class, (method, args) -> { if ("insert".equals(method)) { logs.add((ElnFormulaCalculationLogEntity) args[0]); return 1; } return null; }); service = new ElnRecordFormulaCalculationServiceImpl( formulaMapper, versionMapper, slotMapper, bindingMapper, logMapper, new FormulaPreviewServiceImpl(new FormulaValidationServiceImpl()), new FormulaDependencyGraph()); } private ElnRecordFieldValueEntity value(String bindingId) { for (ElnRecordFieldValueEntity value : values) { if (bindingId.equals(value.getMubanBangdingId())) return value; } throw new AssertionError(bindingId); } private ElnRecordFieldValueEntity value(String bindingId, BigDecimal decimal) { ElnRecordFieldValueEntity value = new ElnRecordFieldValueEntity(); value.setId("value-" + bindingId); value.setDianziJiluId(record.getId()); value.setXiudingId(revision.getId()); value.setMubanId(record.getMubanId()); value.setMubanBangdingId(bindingId); value.setZiduanDingyiId("field-" + bindingId); value.setZiduanBianma("code-" + bindingId); value.setZiduanBanbenId("field-version-" + bindingId); value.setBangdingJuese("PRIMARY"); value.setBangdingZhuangtai("ACTIVE"); value.setZhiShuzhi(decimal); value.setZhiWenben(decimal == null ? null : decimal.toPlainString()); if (bindingId.startsWith("result-")) value.setJisuanZhuangtai("SUCCESS"); return value; } } private static ElnTemplateFormulaBindingEntity formula( String id, String definitionId, String versionId, String inputBindingId, String resultBindingId) { ElnTemplateFormulaBindingEntity formula = new ElnTemplateFormulaBindingEntity(); formula.setId(id); formula.setMubanId("template-1"); formula.setGongshiDingyiId(definitionId); formula.setGongshiBanbenId(versionId); formula.setJieguoBangdingId(resultBindingId); formula.setBangdingZhuangtai("ACTIVE"); Map mappings = new LinkedHashMap<>(); mappings.put("x", inputBindingId); formula.setCanshuYingshe(mappings); return formula; } private static ElnTemplateBindingEntity binding(String id) { ElnTemplateBindingEntity binding = new ElnTemplateBindingEntity(); binding.setId(id); binding.setMubanId("template-1"); binding.setZiduanDingyiId("field-" + id); binding.setZiduanBianma("code-" + id); binding.setZiduanBanbenId("field-version-" + id); binding.setKongjianJuese("PRIMARY"); binding.setBangdingZhuangtai("ACTIVE"); return binding; } private static ElnFormulaVersionEntity version(String id, String definitionId) { ElnFormulaVersionEntity version = new ElnFormulaVersionEntity(); version.setId(id); version.setGongshiDingyiId(definitionId); version.setBiaodashi("x + 1"); version.setJieguoLeixing("DECIMAL"); version.setBizStatus("PUBLISHED"); return version; } private static ElnFormulaParameterSlotEntity slot(String versionId) { ElnFormulaParameterSlotEntity slot = new ElnFormulaParameterSlotEntity(); slot.setId("slot-" + versionId); slot.setGongshiBanbenId(versionId); slot.setCanshuBianma("x"); slot.setCanshuMingcheng("输入"); slot.setShujuLeixing("DECIMAL"); slot.setShifouBitian(1); slot.setPaixu(1); return slot; } @SuppressWarnings("unchecked") private static T proxy(Class type, Invocation invocation) { return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, (target, method, args) -> { if (method.getDeclaringClass() == Object.class) { if ("toString".equals(method.getName())) return type.getSimpleName(); if ("hashCode".equals(method.getName())) return System.identityHashCode(target); if ("equals".equals(method.getName())) return target == args[0]; } Object result = invocation.invoke(method.getName(), args); if (result != null || !method.getReturnType().isPrimitive()) return result; if (method.getReturnType() == boolean.class) return false; return 0; }); } @FunctionalInterface private interface Invocation { Object invoke(String method, Object[] args); } }