From 545b9baad2efa9d6aad3cd67063fe0c60f9e9a19 Mon Sep 17 00:00:00 2001 From: lance Date: Sun, 19 Jul 2026 09:09:59 +0800 Subject: [PATCH] Add option to omit failed values from Data Validator log messages Signed-off-by: lance --- .../hop/pipeline/transform/BaseTransform.java | 16 +- .../transforms/validator/Validator.java | 53 +- .../transforms/validator/ValidatorDialog.java | 65 +- .../transforms/validator/ValidatorMeta.java | 127 +-- .../messages/messages_en_US.properties | 3 + .../messages/messages_zh_CN.properties | 3 + .../validator/HopValidatorExceptionTest.java | 135 +++ .../transforms/validator/ValidationTest.java | 157 ++++ .../validator/ValidatorDataTest.java | 42 + .../ValidatorDialogSelectionTest.java | 93 ++ .../validator/ValidatorMetaTest.java | 282 ++++++ .../transforms/validator/ValidatorTest.java | 865 ++++++++++++++++++ .../test/resources/validator-transform.xml | 60 ++ 13 files changed, 1770 insertions(+), 131 deletions(-) create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/HopValidatorExceptionTest.java create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidationTest.java create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDataTest.java create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialogSelectionTest.java create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorMetaTest.java create mode 100644 plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorTest.java create mode 100644 plugins/transforms/validator/src/test/resources/validator-transform.xml diff --git a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java index 23d19ebc0af..5600a65210d 100644 --- a/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java +++ b/engine/src/main/java/org/apache/hop/pipeline/transform/BaseTransform.java @@ -1417,6 +1417,7 @@ public void handlePutRowTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet) try { Thread.sleep(1); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new HopTransformException(e); } } @@ -1523,7 +1524,7 @@ public void handlePutError( } } incrementLinesRejected(); - if (!Utils.isEmpty(errorDescriptions)) { + if (isLoggingErrorDescriptions() && !Utils.isEmpty(errorDescriptions)) { logError(errorDescriptions); } } else if (transformErrorMeta.isEnabled()) { @@ -2723,6 +2724,19 @@ public void logRowlevel(String message, Object... arguments) { log.logRowlevel(message, arguments); } + /** + * Whether rejected-row error descriptions should be written to the log. + * + *

Transforms that can produce a very large number of validation/rejection errors may override + * this to reduce log volume. Error rows are still sent to the error handling hop when configured. + * + * @return true when error descriptions should be logged (default) + * @since 2.19.0 + */ + protected boolean isLoggingErrorDescriptions() { + return true; + } + /** * Log error. * diff --git a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/Validator.java b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/Validator.java index f4257dde018..191fdcc4c8b 100644 --- a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/Validator.java +++ b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/Validator.java @@ -47,6 +47,9 @@ public class Validator extends BaseTransform implements ITransform { private static final Class PKG = ValidatorMeta.class; + /** Placeholder used in log/error messages when failed data must not be exposed. */ + static final String OMITTED_VALUE = "?"; + public Validator( TransformMeta transformMeta, ValidatorMeta meta, @@ -279,7 +282,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.NullNotAllowed", field.getFieldName(), - inputRowMeta.getString(r)), + getRowForMessage(inputRowMeta, r)), field.getFieldName()); exceptions.add(exception); if (!meta.isValidatingAll()) { @@ -297,7 +300,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.OnlyNullAllowed", field.getFieldName(), - inputRowMeta.getString(r)), + getRowForMessage(inputRowMeta, r)), field.getFieldName()); exceptions.add(exception); if (!meta.isValidatingAll()) { @@ -367,7 +370,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.ShorterThanMininumLength", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), Integer.toString(stringValue.length()), field.getMinimumLength()), field.getFieldName()); @@ -390,7 +393,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.LongerThanMaximumLength", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), Integer.toString(stringValue.length()), field.getMaximumLength()), field.getFieldName()); @@ -413,7 +416,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.LowerThanMinimumValue", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), data.constantsMeta[i].getString(data.minimumValue[i])), field.getFieldName()); exceptions.add(exception); @@ -435,7 +438,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.HigherThanMaximumValue", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), data.constantsMeta[i].getString(data.maximumValue[i])), field.getFieldName()); exceptions.add(exception); @@ -465,7 +468,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.NotInList", field.getFieldName(), - valueMeta.getString(valueData)), + getValueForMessage(valueMeta, valueData)), field.getFieldName()); exceptions.add(exception); if (!meta.isValidatingAll()) { @@ -498,7 +501,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.DoesNotStartWithString", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), field.getStartString()), field.getFieldName()); exceptions.add(exception); @@ -519,7 +522,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.DoesNotEndWithString", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), field.getEndString()), field.getFieldName()); exceptions.add(exception); @@ -541,7 +544,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.StartsWithString", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), field.getStartStringNotAllowed()), field.getFieldName()); exceptions.add(exception); @@ -563,7 +566,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.EndsWithString", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), field.getEndStringNotAllowed()), field.getFieldName()); exceptions.add(exception); @@ -586,7 +589,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.MatchingRegExpExpected", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), data.regularExpression[i]), field.getFieldName()); exceptions.add(exception); @@ -610,7 +613,7 @@ private List validateFields(IRowMeta inputRowMeta, Object PKG, "Validator.Exception.MatchingRegExpNotAllowed", field.getFieldName(), - valueMeta.getString(valueData), + getValueForMessage(valueMeta, valueData), data.regularExpressionNotAllowed[i]), field.getFieldName()); exceptions.add(exception); @@ -625,6 +628,28 @@ private List validateFields(IRowMeta inputRowMeta, Object return exceptions; } + /** + * Returns the field value for inclusion in validation messages, or a placeholder when logging of + * failed data is suppressed. + */ + String getValueForMessage(IValueMeta valueMeta, Object valueData) throws HopValueException { + if (meta.isSuppressingLogFailedData()) { + return OMITTED_VALUE; + } + return valueMeta.getString(valueData); + } + + /** + * Returns the row for inclusion in validation messages, or a placeholder when logging of failed + * data is suppressed. + */ + String getRowForMessage(IRowMeta inputRowMeta, Object[] r) throws HopValueException { + if (meta.isSuppressingLogFailedData()) { + return OMITTED_VALUE; + } + return inputRowMeta.getString(r); + } + // package-local visibility for testing purposes HopValidatorException assertNumeric(IValueMeta valueMeta, Object valueData, Validation field) throws HopValueException { @@ -640,7 +665,7 @@ HopValidatorException assertNumeric(IValueMeta valueMeta, Object valueData, Vali "Validator.Exception.NonNumericDataNotAllowed", field.getFieldName(), valueMeta.toStringMeta(), - valueMeta.getString(valueData)), + getValueForMessage(valueMeta, valueData)), field.getFieldName()); } diff --git a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialog.java b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialog.java index c6cbf240341..861318cb156 100644 --- a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialog.java +++ b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialog.java @@ -113,6 +113,7 @@ public class ValidatorDialog extends BaseTransformDialog { private TextVar wErrorDescription; private Button wConcatErrors; private TextVar wConcatSeparator; + private Button wSuppressLogFailedData; public ValidatorDialog( Shell parent, IVariables variables, ValidatorMeta transformMeta, PipelineMeta pipelineMeta) { @@ -207,6 +208,20 @@ public String open() { fdConcatSeparator.top = new FormAttachment(wValidateAll, margin); wConcatSeparator.setLayoutData(fdConcatSeparator); + // Optionally suppress logging of failed validation data (reduces log volume) + // + wSuppressLogFailedData = new Button(shell, SWT.CHECK); + wSuppressLogFailedData.setText( + BaseMessages.getString(PKG, "ValidatorDialog.SuppressLogFailedData.Label")); + wSuppressLogFailedData.setToolTipText( + BaseMessages.getString(PKG, "ValidatorDialog.SuppressLogFailedData.Tooltip")); + PropsUi.setLook(wSuppressLogFailedData); + FormData fdSuppressLogFailedData = new FormData(); + fdSuppressLogFailedData.left = new FormAttachment(middle, 0); + fdSuppressLogFailedData.right = new FormAttachment(100, 0); + fdSuppressLogFailedData.top = new FormAttachment(wConcatErrors, margin); + wSuppressLogFailedData.setLayoutData(fdSuppressLogFailedData); + // Create a scrolled composite on the right side... // ScrolledComposite wSComp = new ScrolledComposite(shell, SWT.H_SCROLL | SWT.V_SCROLL); @@ -214,7 +229,7 @@ public String open() { wSComp.setLayout(new FillLayout()); FormData fdComp = new FormData(); fdComp.left = new FormAttachment(middle / 2, margin); - fdComp.top = new FormAttachment(wConcatSeparator, margin); + fdComp.top = new FormAttachment(wSuppressLogFailedData, margin); fdComp.right = new FormAttachment(100, -margin); fdComp.bottom = new FormAttachment(wOk, -margin); // Limit viewport size so the dialog opens at a reasonable size; content scrolls inside. @@ -344,8 +359,8 @@ public String open() { PropsUi.setLook(wgType); wgType.setText(BaseMessages.getString(PKG, "ValidatorDialog.TypeGroup.Label")); FormLayout typeGroupLayout = new FormLayout(); - typeGroupLayout.marginHeight = Const.FORM_MARGIN; - typeGroupLayout.marginWidth = Const.FORM_MARGIN; + typeGroupLayout.marginHeight = PropsUi.getFormMargin(); + typeGroupLayout.marginWidth = PropsUi.getFormMargin(); wgType.setLayout(typeGroupLayout); FormData fdType = new FormData(); fdType.left = new FormAttachment(0, 0); @@ -893,10 +908,6 @@ private void saveChanges() { selectedField.setSourcingField(wSourceField.getText()); selectedField.setSourcingTransformName(wSourceTransform.getText()); selectedField.setSourcingTransform(pipelineMeta.findTransform(wSourceTransform.getText())); - - // Save the old info in the map - // - // selectionList.add(selectedField); } } @@ -1044,11 +1055,12 @@ public void getData() { wValidateAll.setSelection(input.isValidatingAll()); wConcatErrors.setSelection(input.isConcatenatingErrors()); wConcatSeparator.setText(Const.NVL(input.getConcatenationSeparator(), "")); + wSuppressLogFailedData.setSelection(input.isSuppressingLogFailedData()); // Select the first available field... // if (!input.getValidations().isEmpty()) { - Validation validatorField = input.getValidations().get(0); + Validation validatorField = input.getValidations().getFirst(); String description = validatorField.getName(); int index = wValidationsList.indexOf(description); if (index >= 0) { @@ -1085,6 +1097,7 @@ private void ok() { input.setValidatingAll(wValidateAll.getSelection()); input.setConcatenatingErrors(wConcatErrors.getSelection()); input.setConcatenationSeparator(wConcatSeparator.getText()); + input.setSuppressingLogFailedData(wSuppressLogFailedData.getSelection()); input.setValidations(selectionList); @@ -1169,17 +1182,37 @@ private void newValidation() { /** Clear the validation rules for a certain field. */ private void clearValidation() { - int index = wValidationsList.getSelectionIndex(); - if (index >= 0) { - selectionList.remove(index); - selectedField = null; - wValidationsList.remove(index); + if (index < 0) { + return; + } + + selectionList.remove(index); + selectedField = null; + wValidationsList.remove(index); + + if (selectionList.isEmpty()) { enableFields(); + return; + } - if (!selectionList.isEmpty()) { - wValidationsList.select(selectionList.size() - 1); - } + // Keep showing the nearest remaining rule (same index, or previous when deleting the last) + int newIndex = indexAfterRemoval(index, selectionList.size()); + wValidationsList.select(newIndex); + showSelectedValidatorField(wValidationsList.getItem(newIndex)); + } + + /** + * Calculate which list index to select after removing an item. + * + * @param removedIndex the index that was removed + * @param remainingCount the number of items left after removal + * @return the index to select, or -1 when nothing remains + */ + static int indexAfterRemoval(int removedIndex, int remainingCount) { + if (remainingCount <= 0) { + return -1; } + return Math.min(removedIndex, remainingCount - 1); } } diff --git a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorMeta.java b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorMeta.java index c6cf5813ac0..c8ccb6407a3 100644 --- a/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorMeta.java +++ b/plugins/transforms/validator/src/main/java/org/apache/hop/pipeline/transforms/validator/ValidatorMeta.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; +import lombok.Getter; +import lombok.Setter; import org.apache.hop.core.CheckResult; import org.apache.hop.core.Const; import org.apache.hop.core.ICheckResult; @@ -38,6 +40,8 @@ import org.apache.hop.pipeline.transform.stream.Stream; import org.apache.hop.pipeline.transform.stream.StreamIcon; +@Getter +@Setter @Transform( id = "Validator", name = "i18n::ValidatorDialog.Transform.Name", @@ -76,6 +80,27 @@ public class ValidatorMeta extends BaseTransformMeta { injectionKeyDescription = "Validator.Injection.CONCATENATION_SEPARATOR") private String concatenationSeparator; + /** + * When true, failed field/row values are omitted from validation error messages. Errors are still + * logged and error rows are still produced for error handling. Defaults to false so existing + * pipelines keep including failed values in messages. + */ + @HopMetadataProperty( + key = "suppress_log_failed_data", + injectionKey = "SUPPRESS_LOG_FAILED_DATA", + injectionKeyDescription = "Validator.Injection.SUPPRESS_LOG_FAILED_DATA") + private boolean suppressingLogFailedData; + + /** The standard new validation stream */ + @Getter @Setter + private static IStream newValidation = + new Stream( + IStream.StreamType.INFO, + null, + BaseMessages.getString(PKG, "ValidatorMeta.NewValidation.Description"), + StreamIcon.INFO, + null); + public ValidatorMeta() { this.validations = new ArrayList<>(); } @@ -86,6 +111,7 @@ public ValidatorMeta(ValidatorMeta m) { this.validatingAll = m.validatingAll; this.concatenatingErrors = m.concatenatingErrors; this.concatenationSeparator = m.concatenationSeparator; + this.suppressingLogFailedData = m.suppressingLogFailedData; } @Override @@ -170,7 +196,7 @@ public ITransformIOMeta getTransformIOMeta() { } @Override - public void searchInfoAndTargetTransforms(List transforms) { + public void searchInfoAndTargetTransforms(List transforms) { for (Validation validation : validations) { TransformMeta transformMeta = TransformMeta.findTransform(transforms, validation.getSourcingTransformName()); @@ -179,15 +205,6 @@ public void searchInfoAndTargetTransforms(List transforms) { resetTransformIoMeta(); } - /** The standard new validation stream */ - private static IStream newValidation = - new Stream( - IStream.StreamType.INFO, - null, - BaseMessages.getString(PKG, "ValidatorMeta.NewValidation.Description"), - StreamIcon.INFO, - null); - @Override public void handleStreamSelection(IStream stream) { // A hack to prevent us from losing information in the UI because @@ -212,94 +229,4 @@ public void handleStreamSelection(IStream stream) { // Force the IO to be recreated when it is next needed. resetTransformIoMeta(); } - - /** - * Gets validations - * - * @return value of validations - */ - public List getValidations() { - return validations; - } - - /** - * Sets validations - * - * @param validations value of validations - */ - public void setValidations(List validations) { - this.validations = validations; - } - - /** - * Gets validatingAll - * - * @return value of validatingAll - */ - public boolean isValidatingAll() { - return validatingAll; - } - - /** - * Sets validatingAll - * - * @param validatingAll value of validatingAll - */ - public void setValidatingAll(boolean validatingAll) { - this.validatingAll = validatingAll; - } - - /** - * Gets concatenatingErrors - * - * @return value of concatenatingErrors - */ - public boolean isConcatenatingErrors() { - return concatenatingErrors; - } - - /** - * Sets concatenatingErrors - * - * @param concatenatingErrors value of concatenatingErrors - */ - public void setConcatenatingErrors(boolean concatenatingErrors) { - this.concatenatingErrors = concatenatingErrors; - } - - /** - * Gets concatenationSeparator - * - * @return value of concatenationSeparator - */ - public String getConcatenationSeparator() { - return concatenationSeparator; - } - - /** - * Sets concatenationSeparator - * - * @param concatenationSeparator value of concatenationSeparator - */ - public void setConcatenationSeparator(String concatenationSeparator) { - this.concatenationSeparator = concatenationSeparator; - } - - /** - * Gets newValidation - * - * @return value of newValidation - */ - public static IStream getNewValidation() { - return newValidation; - } - - /** - * Sets newValidation - * - * @param newValidation value of newValidation - */ - public static void setNewValidation(IStream newValidation) { - ValidatorMeta.newValidation = newValidation; - } } diff --git a/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_en_US.properties b/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_en_US.properties index b79d97aa41a..f81ecbec9ab 100644 --- a/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_en_US.properties +++ b/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_en_US.properties @@ -38,6 +38,7 @@ Validator.Exception.StartsWithString=During validation of field ''{0}'' we found Validator.Exception.UnexpectedDataType=During validation of field ''{0}'' we found that its data type [{1}] is different from the expected [{2}]. Validator.Injection.CONCATENATE_ERRORS=This option causes all errors to be combined into a single output row. Validator.Injection.CONCATENATION_SEPARATOR=Specify the error separator when the errors are in one output row. +Validator.Injection.SUPPRESS_LOG_FAILED_DATA=When enabled, failed field/row values are omitted from validation error messages written to the log. Error rows are still sent to the error handling hop. Validator.Injection.CONVERSION_MASK=Specify the mask to use to convert the data specified in this validation rule. Validator.Injection.DATA_TYPE=Specify the data type to verify. Validator.Injection.DATA_TYPE_VERIFIED=This option causes the specified data type to be verified. @@ -70,6 +71,8 @@ ValidatorDialog.ButtonAddAllowed.Label=\ Add ValidatorDialog.ButtonRemoveAllowed.Label=\ Remove ValidatorDialog.ClearButton.Label=Remove validation ValidatorDialog.ConcatErrors.Label=Output one row, concatenate errors with separator +ValidatorDialog.SuppressLogFailedData.Label=Do not log failed validation data +ValidatorDialog.SuppressLogFailedData.Tooltip=When enabled, failed field/row values are omitted from validation error messages (shown as "?"). Error messages are still logged. Error rows are still sent to the error handling hop when configured. ValidatorDialog.ConversionMask.Label=Conversion mask ValidatorDialog.DataGroup.Label=Data ValidatorDialog.DataType.Label=Data type diff --git a/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_zh_CN.properties b/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_zh_CN.properties index 9f7a1300795..f590eb1006d 100644 --- a/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_zh_CN.properties +++ b/plugins/transforms/validator/src/main/resources/org/apache/hop/pipeline/transforms/validator/messages/messages_zh_CN.properties @@ -23,3 +23,6 @@ Validator.Exception.DataConversionErrorEncountered=\u5728\u8FDB\u884C\u9A8C\u8BC Validator.Exception.DoesNotEndWithString=\u5728\u9A8C\u8BC1\u5B57\u6BB5 ''{0}'' \u65F6\uFF0C\u6211\u4EEC\u53D1\u73B0\u5B57\u7B26\u4E32\u503C [{1}] \u5E76\u672A\u4EE5\u5B57\u7B26\u4E32 [{2}] \u7ED3\u5C3E Validator.Exception.DoesNotStartWithString=\u5728\u9A8C\u8BC1\u5B57\u6BB5 ''{0}'' \u65F6\uFF0C\u5B57\u7B26\u4E32\u503C [{1}] \u5E76\u672A\u4EE5\u5B57\u7B26\u4E32 [{2}] \u5F00\u5934 Validator.Exception.EndsWithString=\u5728\u9A8C\u8BC1\u5B57\u6BB5\u201C{0}\u201D\u65F6\uFF0C\u6211\u4EEC\u53D1\u73B0\u5B57\u7B26\u4E32\u503C [{1}] \u4EE5\u5B57\u7B26\u4E32 [{2}] \u7ED3\u5C3E +Validator.Injection.SUPPRESS_LOG_FAILED_DATA=\u542F\u7528\u540E\uFF0C\u5199\u5165\u65E5\u5FD7\u7684\u6821\u9A8C\u9519\u8BEF\u6D88\u606F\u4E2D\u4E0D\u5305\u542B\u5931\u8D25\u5B57\u6BB5/\u884C\u6570\u636E\u3002\u9519\u8BEF\u884C\u4ECD\u4F1A\u53D1\u9001\u5230\u9519\u8BEF\u5904\u7406\u8DEF\u5F84\u3002 +ValidatorDialog.SuppressLogFailedData.Label=\u4E0D\u8BB0\u5F55\u6821\u9A8C\u5931\u8D25\u7684\u6570\u636E +ValidatorDialog.SuppressLogFailedData.Tooltip=\u542F\u7528\u540E\uFF0C\u6821\u9A8C\u9519\u8BEF\u6D88\u606F\u4E2D\u7684\u5931\u8D25\u5B57\u6BB5/\u884C\u6570\u636E\u4F1A\u88AB\u7701\u7565\uFF08\u663E\u793A\u4E3A\u201C?\u201D\uFF09\uFF0C\u4F46\u4ECD\u4F1A\u6253\u5370\u9519\u8BEF\u65E5\u5FD7\u3002\u82E5\u914D\u7F6E\u4E86\u9519\u8BEF\u5904\u7406\uFF0C\u9519\u8BEF\u884C\u4ECD\u4F1A\u53D1\u9001\u5230\u9519\u8BEF\u8DEF\u5F84\u3002 diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/HopValidatorExceptionTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/HopValidatorExceptionTest.java new file mode 100644 index 00000000000..fa8cd4d77af --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/HopValidatorExceptionTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link HopValidatorException}. */ +class HopValidatorExceptionTest { + + private Validator validator; + + @BeforeEach + void setUp() { + validator = mock(Validator.class); + when(validator.resolve(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + void exposesCodeFieldNameAndValidation() { + Validation field = new Validation(); + field.setName("rule"); + HopValidatorException exception = + new HopValidatorException( + validator, + field, + HopValidatorException.ERROR_NULL_VALUE_NOT_ALLOWED, + "null not allowed", + "value"); + + assertEquals(HopValidatorException.ERROR_NULL_VALUE_NOT_ALLOWED, exception.getCode()); + assertEquals("value", exception.getFieldName()); + assertSame(field, exception.getValidatorField()); + assertTrue(exception.getMessage().contains("null not allowed")); + } + + @Test + void getCodeDescUsesBuiltInCodeWhenNoCustomErrorCode() { + Validation field = new Validation(); + HopValidatorException exception = + new HopValidatorException( + validator, + field, + HopValidatorException.ERROR_LONGER_THAN_MAXIMUM_LENGTH, + "too long", + "value"); + + assertEquals("KVD002", exception.getCodeDesc()); + } + + @Test + void getCodeDescUsesResolvedCustomErrorCode() { + Validation field = new Validation(); + field.setErrorCode("${ERR_CODE}"); + when(validator.resolve("${ERR_CODE}")).thenReturn("CUSTOM_CODE"); + + HopValidatorException exception = + new HopValidatorException( + validator, + field, + HopValidatorException.ERROR_VALUE_NOT_IN_LIST, + "not in list", + "value"); + + assertEquals("CUSTOM_CODE", exception.getCodeDesc()); + } + + @Test + void getMessageUsesResolvedCustomErrorDescription() { + Validation field = new Validation(); + field.setErrorDescription("${ERR_DESC}"); + when(validator.resolve("${ERR_DESC}")).thenReturn("custom description"); + + HopValidatorException exception = + new HopValidatorException( + validator, + field, + HopValidatorException.ERROR_SHORTER_THAN_MINIMUM_LENGTH, + "built-in message", + "value"); + + assertTrue(exception.getMessage().contains("custom description")); + } + + @Test + void getMessageFallsBackToBuiltInMessage() { + Validation field = new Validation(); + HopValidatorException exception = + new HopValidatorException( + validator, + field, + HopValidatorException.ERROR_ONLY_NULL_VALUE_ALLOWED, + "built-in message", + "value"); + + assertTrue(exception.getMessage().contains("built-in message")); + } + + @Test + void allBuiltInErrorCodesAreMapped() { + Validation field = new Validation(); + String[] expected = { + "KVD000", "KVD001", "KVD002", "KVD003", "KVD004", "KVD005", "KVD006", "KVD007", "KVD008", + "KVD009", "KVD010", "KVD011", "KVD012", "KVD013", "KVD014", "KVD015", + }; + + for (int code = 0; code < expected.length; code++) { + HopValidatorException exception = + new HopValidatorException(validator, field, code, "msg", "field"); + assertEquals(expected[code], exception.getCodeDesc(), "code " + code); + } + } +} diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidationTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidationTest.java new file mode 100644 index 00000000000..02e2a782dae --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidationTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link Validation}. */ +class ValidationTest { + + @Test + void defaultConstructorSetsSafeDefaults() { + Validation validation = new Validation(); + + assertTrue(validation.isNullAllowed()); + assertFalse(validation.isOnlyNullAllowed()); + assertFalse(validation.isOnlyNumericAllowed()); + assertEquals("", validation.getMaximumLength()); + assertEquals("", validation.getMinimumLength()); + assertNotNull(validation.getAllowedValues()); + assertTrue(validation.getAllowedValues().isEmpty()); + } + + @Test + void nameConstructorSetsFieldName() { + Validation validation = new Validation("email"); + assertEquals("email", validation.getFieldName()); + assertTrue(validation.isNullAllowed()); + } + + @Test + void copyConstructorAndCloneAreDeepForAllowedValues() { + Validation original = new Validation(); + original.setName("rule"); + original.setFieldName("field"); + original.setMaximumLength("10"); + original.setMinimumLength("1"); + original.setNullAllowed(false); + original.setOnlyNullAllowed(true); + original.setOnlyNumericAllowed(true); + original.setDataType("String"); + original.setDataTypeVerified(true); + original.setConversionMask("#"); + original.setDecimalSymbol("."); + original.setGroupingSymbol(","); + original.setSourcingValues(true); + original.setSourcingTransformName("source"); + original.setSourcingField("allowed"); + original.setMinimumValue("1"); + original.setMaximumValue("9"); + original.setStartString("A"); + original.setEndString("Z"); + original.setStartStringNotAllowed("X"); + original.setEndStringNotAllowed("Y"); + original.setRegularExpression(".*"); + original.setRegularExpressionNotAllowed("bad"); + original.setErrorCode("E1"); + original.setErrorDescription("desc"); + original.setAllowedValues(new ArrayList<>(List.of("A", "B"))); + TransformMeta sourcing = new TransformMeta(); + sourcing.setName("source"); + original.setSourcingTransform(sourcing); + + Validation copy = new Validation(original); + Validation clone = original.clone(); + + assertNotSame(original, copy); + assertNotSame(original.getAllowedValues(), copy.getAllowedValues()); + assertEquals(original.getName(), copy.getName()); + assertEquals(original.getFieldName(), copy.getFieldName()); + assertEquals(original.getMaximumLength(), copy.getMaximumLength()); + assertEquals(original.getMinimumLength(), copy.getMinimumLength()); + assertEquals(original.isNullAllowed(), copy.isNullAllowed()); + assertEquals(original.isOnlyNullAllowed(), copy.isOnlyNullAllowed()); + assertEquals(original.isOnlyNumericAllowed(), copy.isOnlyNumericAllowed()); + assertEquals(original.getDataType(), copy.getDataType()); + assertEquals(original.isDataTypeVerified(), copy.isDataTypeVerified()); + assertEquals(original.getConversionMask(), copy.getConversionMask()); + assertEquals(original.getDecimalSymbol(), copy.getDecimalSymbol()); + assertEquals(original.getGroupingSymbol(), copy.getGroupingSymbol()); + assertEquals(original.isSourcingValues(), copy.isSourcingValues()); + assertEquals(original.getSourcingTransformName(), copy.getSourcingTransformName()); + assertEquals(original.getSourcingField(), copy.getSourcingField()); + assertEquals(original.getMinimumValue(), copy.getMinimumValue()); + assertEquals(original.getMaximumValue(), copy.getMaximumValue()); + assertEquals(original.getStartString(), copy.getStartString()); + assertEquals(original.getEndString(), copy.getEndString()); + assertEquals(original.getStartStringNotAllowed(), copy.getStartStringNotAllowed()); + assertEquals(original.getEndStringNotAllowed(), copy.getEndStringNotAllowed()); + assertEquals(original.getRegularExpression(), copy.getRegularExpression()); + assertEquals(original.getRegularExpressionNotAllowed(), copy.getRegularExpressionNotAllowed()); + assertEquals(original.getErrorCode(), copy.getErrorCode()); + assertEquals(original.getErrorDescription(), copy.getErrorDescription()); + assertEquals(original.getAllowedValues(), copy.getAllowedValues()); + assertSameSourcingTransform(original, copy); + + assertEquals(original.getName(), clone.getName()); + assertNotSame(original.getAllowedValues(), clone.getAllowedValues()); + + copy.getAllowedValues().add("C"); + assertEquals(2, original.getAllowedValues().size()); + assertEquals(3, copy.getAllowedValues().size()); + } + + @Test + void equalsIgnoresCaseOnName() { + Validation left = new Validation(); + left.setName("RuleOne"); + Validation right = new Validation(); + right.setName("ruleone"); + + assertTrue(left.equals(right)); + assertTrue(right.equals(left)); + } + + @Test + void findValidationMatchesCaseInsensitively() { + Validation first = new Validation(); + first.setName("Alpha"); + Validation second = new Validation(); + second.setName("Beta"); + List list = List.of(first, second); + + assertEquals(first, Validation.findValidation(list, "alpha")); + assertEquals(second, Validation.findValidation(list, "BETA")); + assertNull(Validation.findValidation(list, "gamma")); + } + + private static void assertSameSourcingTransform(Validation original, Validation copy) { + assertSame(original.getSourcingTransform(), copy.getSourcingTransform()); + } +} diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDataTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDataTest.java new file mode 100644 index 00000000000..479853ccb81 --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDataTest.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.apache.hop.pipeline.transform.ITransformData; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ValidatorData}. */ +class ValidatorDataTest { + + @Test + void constructorCreatesUsableTransformData() { + ValidatorData data = new ValidatorData(); + + assertNotNull(data); + assertInstanceOf(ITransformData.class, data); + assertNull(data.fieldIndexes); + assertNull(data.constantsMeta); + assertNull(data.inputRowMeta); + assertNull(data.patternExpected); + assertNull(data.patternDisallowed); + } +} diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialogSelectionTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialogSelectionTest.java new file mode 100644 index 00000000000..00fcd704f82 --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorDialogSelectionTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for non-UI logic in {@link ValidatorDialog}. Full SWT dialog coverage is out of scope + * for headless unit tests. + */ +class ValidatorDialogSelectionTest { + + private ValidatorDialog dialog; + + @BeforeEach + void setUp() { + dialog = mock(ValidatorDialog.class); + when(dialog.spacesValidation(anyString())).thenCallRealMethod(); + when(dialog.spacesValidation(null)).thenCallRealMethod(); + when(dialog.trailingSpacesValidation(anyString())).thenCallRealMethod(); + when(dialog.trailingSpacesValidation(null)).thenCallRealMethod(); + } + + @Test + void indexAfterRemovalSelectsSameIndexWhenItemsRemainAfter() { + // Remove index 0 from [A,B,C] -> remaining [B,C], select B (index 0) + assertEquals(0, ValidatorDialog.indexAfterRemoval(0, 2)); + } + + @Test + void indexAfterRemovalSelectsPreviousWhenLastItemRemoved() { + // Remove index 2 from [A,B,C] -> remaining [A,B], select B (index 1) + assertEquals(1, ValidatorDialog.indexAfterRemoval(2, 2)); + } + + @Test + void indexAfterRemovalSelectsMiddleNeighbor() { + // Remove index 1 from [A,B,C] -> remaining [A,C], select C (index 1) + assertEquals(1, ValidatorDialog.indexAfterRemoval(1, 2)); + } + + @Test + void indexAfterRemovalReturnsMinusOneWhenEmpty() { + assertEquals(-1, ValidatorDialog.indexAfterRemoval(0, 0)); + } + + @Test + void indexAfterRemovalKeepsSingleRemainingItem() { + assertEquals(0, ValidatorDialog.indexAfterRemoval(0, 1)); + assertEquals(0, ValidatorDialog.indexAfterRemoval(5, 1)); + } + + @Test + void spacesValidationDetectsOnlySpaces() { + assertTrue(dialog.spacesValidation(" ")); + assertFalse(dialog.spacesValidation("a")); + assertFalse(dialog.spacesValidation(" a ")); + assertFalse(dialog.spacesValidation("")); + assertFalse(dialog.spacesValidation(null)); + } + + @Test + void trailingSpacesValidationDetectsTrailingSpace() { + assertTrue(dialog.trailingSpacesValidation("value ")); + assertTrue(dialog.trailingSpacesValidation(" ")); + assertFalse(dialog.trailingSpacesValidation("value")); + assertFalse(dialog.trailingSpacesValidation("")); + assertFalse(dialog.trailingSpacesValidation(null)); + } +} diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorMetaTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorMetaTest.java new file mode 100644 index 00000000000..c65f32fbca2 --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorMetaTest.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.ICheckResult; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.ITransformIOMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transform.TransformSerializationTestUtil; +import org.apache.hop.pipeline.transform.stream.IStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit test for {@link ValidatorMeta} */ +class ValidatorMetaTest { + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + @BeforeEach + void setUp() throws Exception { + HopEnvironment.init(); + PluginRegistry.init(); + } + + @Test + void testSerialization() throws Exception { + ValidatorMeta meta = + TransformSerializationTestUtil.testSerialization( + "/validator-transform.xml", ValidatorMeta.class); + + assertTrue(meta.isValidatingAll()); + assertTrue(meta.isConcatenatingErrors()); + assertEquals(";", meta.getConcatenationSeparator()); + assertTrue(meta.isSuppressingLogFailedData()); + assertEquals(1, meta.getValidations().size()); + + Validation validation = meta.getValidations().getFirst(); + assertEquals("value rules", validation.getName()); + assertEquals("value", validation.getFieldName()); + assertFalse(validation.isNullAllowed()); + assertEquals("10", validation.getMaximumLength()); + assertEquals("1", validation.getMinimumLength()); + assertEquals("CODE_NULL", validation.getErrorCode()); + assertEquals(2, validation.getAllowedValues().size()); + assertEquals("A", validation.getAllowedValues().get(0)); + assertEquals("B", validation.getAllowedValues().get(1)); + } + + @Test + void testCloneCopiesOptionsAndValidations() { + ValidatorMeta meta = new ValidatorMeta(); + meta.setValidatingAll(true); + meta.setConcatenatingErrors(true); + meta.setConcatenationSeparator("|"); + meta.setSuppressingLogFailedData(true); + + Validation validation = new Validation(); + validation.setName("rule1"); + validation.setFieldName("field1"); + validation.setNullAllowed(false); + meta.getValidations().add(validation); + + ValidatorMeta clone = meta.clone(); + assertNotSame(meta, clone); + assertNotSame(meta.getValidations(), clone.getValidations()); + assertNotSame(meta.getValidations().getFirst(), clone.getValidations().getFirst()); + assertTrue(clone.isValidatingAll()); + assertTrue(clone.isConcatenatingErrors()); + assertEquals("|", clone.getConcatenationSeparator()); + assertTrue(clone.isSuppressingLogFailedData()); + assertEquals(1, clone.getValidations().size()); + assertEquals("rule1", clone.getValidations().getFirst().getName()); + assertEquals("field1", clone.getValidations().getFirst().getFieldName()); + } + + @Test + void testSupportsErrorHandling() { + assertTrue(new ValidatorMeta().supportsErrorHandling()); + } + + @Test + void testSuppressLogFailedDataDefaultsToFalse() { + assertFalse(new ValidatorMeta().isSuppressingLogFailedData()); + } + + @Test + void checkNoPrevFieldsAddsWarning() { + ValidatorMeta meta = new ValidatorMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + null, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_WARNING)); + } + + @Test + void checkEmptyPrevFieldsAddsWarning() { + ValidatorMeta meta = new ValidatorMeta(); + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + new RowMeta(), + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_WARNING)); + } + + @Test + void checkWithPrevAndInputAddsOkRemarks() { + ValidatorMeta meta = new ValidatorMeta(); + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("field1")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[] {"in"}, + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertEquals(2, remarks.size()); + assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(0).getType()); + assertEquals(ICheckResult.TYPE_RESULT_OK, remarks.get(1).getType()); + } + + @Test + void checkNoInputAddsError() { + ValidatorMeta meta = new ValidatorMeta(); + RowMeta prev = new RowMeta(); + prev.addValueMeta(new ValueMetaString("field1")); + + List remarks = new ArrayList<>(); + meta.check( + remarks, + mock(PipelineMeta.class), + mock(TransformMeta.class), + prev, + new String[0], + new String[0], + mock(IRowMeta.class), + null, + mock(IHopMetadataProvider.class)); + + assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR)); + } + + @Test + void getTransformIOMetaCreatesInfoStreamPerValidation() { + ValidatorMeta meta = new ValidatorMeta(); + Validation validation = new Validation(); + validation.setName("rule1"); + validation.setSourcingTransformName("source"); + meta.getValidations().add(validation); + + ITransformIOMeta ioMeta = meta.getTransformIOMeta(); + assertNotNull(ioMeta); + assertEquals(1, ioMeta.getInfoStreams().size()); + assertEquals(1, meta.getTransformIOMeta().getInfoStreams().size()); + } + + @Test + void searchInfoAndTargetTransformsResolvesSourcingTransform() { + ValidatorMeta meta = new ValidatorMeta(); + Validation validation = new Validation(); + validation.setName("rule1"); + validation.setSourcingTransformName("source"); + meta.getValidations().add(validation); + + TransformMeta source = new TransformMeta(); + source.setName("source"); + meta.searchInfoAndTargetTransforms(List.of(source)); + + assertSame(source, validation.getSourcingTransform()); + } + + @Test + void searchInfoAndTargetTransformsClearsMissingSource() { + ValidatorMeta meta = new ValidatorMeta(); + Validation validation = new Validation(); + validation.setName("rule1"); + validation.setSourcingTransformName("missing"); + meta.getValidations().add(validation); + + meta.searchInfoAndTargetTransforms(List.of()); + assertNull(validation.getSourcingTransform()); + } + + @Test + void handleStreamSelectionAddsValidationForNewValidationStream() { + ValidatorMeta meta = new ValidatorMeta(); + TransformMeta source = new TransformMeta(); + source.setName("allowed_values"); + + IStream newValidation = ValidatorMeta.getNewValidation(); + newValidation.setTransformMeta(source); + + meta.handleStreamSelection(newValidation); + + assertEquals(1, meta.getValidations().size()); + Validation created = meta.getValidations().getFirst(); + assertEquals("allowed_values", created.getName()); + assertTrue(created.isSourcingValues()); + assertSame(source, created.getSourcingTransform()); + } + + @Test + void handleStreamSelectionIgnoresUnrelatedStream() { + ValidatorMeta meta = new ValidatorMeta(); + IStream other = mock(IStream.class); + + meta.handleStreamSelection(other); + assertTrue(meta.getValidations().isEmpty()); + } + + @Test + void newValidationStreamAccessorRoundTrip() { + IStream original = ValidatorMeta.getNewValidation(); + assertNotNull(original); + + IStream replacement = mock(IStream.class); + try { + ValidatorMeta.setNewValidation(replacement); + assertSame(replacement, ValidatorMeta.getNewValidation()); + } finally { + ValidatorMeta.setNewValidation(original); + } + assertSame(original, ValidatorMeta.getNewValidation()); + } +} diff --git a/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorTest.java b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorTest.java new file mode 100644 index 00000000000..8d7b8b203cf --- /dev/null +++ b/plugins/transforms/validator/src/test/java/org/apache/hop/pipeline/transforms/validator/ValidatorTest.java @@ -0,0 +1,865 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.pipeline.transforms.validator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILoggingObject; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.pipeline.transforms.mock.TransformMockHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit test for {@link Validator} */ +class ValidatorTest { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + private TransformMockHelper helper; + + @BeforeAll + static void setUpBeforeClass() throws HopException { + HopEnvironment.init(); + } + + @BeforeEach + void setUp() throws Exception { + helper = new TransformMockHelper<>("Validator", ValidatorMeta.class, ValidatorData.class); + when(helper.logChannelFactory.create(any(), any(ILoggingObject.class))) + .thenReturn(helper.iLogChannel); + when(helper.pipeline.isRunning()).thenReturn(true); + when(helper.transformMeta.getName()).thenReturn("Validator"); + } + + @AfterEach + void tearDown() { + helper.cleanUp(); + } + + @Test + void testValidRowIsPassedThrough() throws Exception { + ValidatorMeta meta = createMeta(nullNotAllowed()); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"ok"}).doReturn(null).when(validator).getRow(); + doNothing().when(validator).putRow(any(IRowMeta.class), any(Object[].class)); + + assertTrue(validator.processRow()); + verify(validator).putRow(any(IRowMeta.class), eq(new Object[] {"ok"})); + verify(validator, never()) + .putError( + any(IRowMeta.class), + any(Object[].class), + anyLong(), + anyString(), + anyString(), + anyString()); + assertFalse(validator.processRow()); + } + + @Test + void testNullNotAllowedPutsError() throws Exception { + ValidatorMeta meta = createMeta(nullNotAllowed()); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {null}).doReturn(null).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD001")); + } + + @Test + void testNullNotAllowedWithoutErrorHandlingThrows() throws Exception { + ValidatorMeta meta = createMeta(nullNotAllowed()); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, false); + + doReturn(new Object[] {null}).when(validator).getRow(); + + HopException exception = assertThrows(HopException.class, validator::processRow); + assertTrue(exception.getMessage().contains("null")); + } + + @Test + void testMaximumLengthViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("max length"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setMaximumLength("3"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"toolong"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + contains("toolong"), + eq("value"), + eq("KVD002")); + } + + @Test + void testMinimumLengthViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("min length"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setMinimumLength("5"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"abc"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + contains("abc"), + eq("value"), + eq("KVD003")); + } + + @Test + void testValueNotInAllowedList() throws Exception { + Validation rule = new Validation(); + rule.setName("list"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setDataType("String"); + rule.setAllowedValues(List.of("A", "B")); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"C"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + contains("C"), + eq("value"), + eq("KVD007")); + } + + @Test + void testOnlyNumericAllowed() throws Exception { + Validation rule = new Validation(); + rule.setName("numeric"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setOnlyNumericAllowed(true); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"12a"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD008")); + } + + @Test + void testRegularExpressionExpected() throws Exception { + Validation rule = new Validation(); + rule.setName("regex"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setRegularExpression("^[0-9]+$"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"abc"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD013")); + } + + @Test + void testMinimumValueViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("min value"); + rule.setFieldName("amount"); + rule.setNullAllowed(true); + rule.setDataType("Integer"); + rule.setMinimumValue("10"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaInteger("amount")); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {5L}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("amount"), + eq("KVD005")); + } + + @Test + void testValidateAllConcatenatesErrorsIntoOneRow() throws Exception { + Validation lengthRule = new Validation(); + lengthRule.setName("length"); + lengthRule.setFieldName("value"); + lengthRule.setNullAllowed(true); + lengthRule.setMaximumLength("1"); + + Validation onlyNull = new Validation(); + onlyNull.setName("only null"); + onlyNull.setFieldName("value"); + onlyNull.setNullAllowed(true); + onlyNull.setOnlyNullAllowed(true); + + ValidatorMeta meta = createMeta(lengthRule, onlyNull); + meta.setValidatingAll(true); + meta.setConcatenatingErrors(true); + meta.setConcatenationSeparator(";"); + + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"ab"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(2L), + contains(";"), + contains(";"), + contains(";")); + } + + @Test + void testCustomErrorCodeAndDescription() throws Exception { + Validation rule = nullNotAllowed(); + rule.setErrorCode("MY_CODE"); + rule.setErrorDescription("custom description"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {null}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + eq("custom description"), + eq("value"), + eq("MY_CODE")); + } + + @Test + void testSuppressLogFailedDataOmitsValuesFromErrorMessages() throws Exception { + Validation rule = new Validation(); + rule.setName("max length"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setMaximumLength("3"); + + ValidatorMeta meta = createMeta(rule); + meta.setSuppressingLogFailedData(true); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"toolong"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + contains(Validator.OMITTED_VALUE), + eq("value"), + eq("KVD002")); + verify(validator, never()) + .putError( + any(IRowMeta.class), + any(Object[].class), + anyLong(), + contains("toolong"), + anyString(), + anyString()); + } + + @Test + void testSuppressLogFailedDataStillLogsCustomErrorDescription() throws Exception { + Validation rule = nullNotAllowed(); + rule.setErrorCode("name_valid_code"); + rule.setErrorDescription("name_valid_desc"); + + ValidatorMeta meta = createMeta(rule); + meta.setSuppressingLogFailedData(true); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {null}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + eq("name_valid_desc"), + eq("value"), + eq("name_valid_code")); + } + + @Test + void testValueAndRowForMessageRespectSuppressFlag() throws Exception { + ValidatorMeta meta = new ValidatorMeta(); + ValidatorData data = new ValidatorData(); + Validator validator = + new Validator(helper.transformMeta, meta, data, 0, helper.pipelineMeta, helper.pipeline); + + ValueMetaString valueMeta = new ValueMetaString("value"); + RowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(valueMeta); + Object[] row = new Object[] {"secret"}; + + meta.setSuppressingLogFailedData(false); + assertEquals("secret", validator.getValueForMessage(valueMeta, "secret")); + assertEquals(rowMeta.getString(row), validator.getRowForMessage(rowMeta, row)); + + meta.setSuppressingLogFailedData(true); + assertEquals(Validator.OMITTED_VALUE, validator.getValueForMessage(valueMeta, "secret")); + assertEquals(Validator.OMITTED_VALUE, validator.getRowForMessage(rowMeta, row)); + } + + @Test + void testAssertNumericAllowsDigitsAndNumericTypes() throws Exception { + ValidatorMeta meta = new ValidatorMeta(); + ValidatorData data = new ValidatorData(); + Validator validator = + new Validator(helper.transformMeta, meta, data, 0, helper.pipelineMeta, helper.pipeline); + + Validation field = new Validation(); + field.setFieldName("value"); + ValueMetaString stringMeta = new ValueMetaString("value"); + ValueMetaInteger integerMeta = new ValueMetaInteger("value"); + + assertNull(validator.assertNumeric(stringMeta, "12345", field)); + assertNull(validator.assertNumeric(integerMeta, 42L, field)); + HopValidatorException exception = validator.assertNumeric(stringMeta, "12a", field); + assertEquals(HopValidatorException.ERROR_NON_NUMERIC_DATA, exception.getCode()); + } + + @Test + void testStartAndEndStringRules() throws Exception { + Validation rule = new Validation(); + rule.setName("prefix"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setStartString("AB"); + rule.setEndString("Z"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = stringRowMeta(); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {"ABX"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD010")); + } + + @Test + void testDoesNotStartWithExpectedString() throws Exception { + Validation rule = new Validation(); + rule.setName("start"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setStartString("AB"); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"XX"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD009")); + } + + @Test + void testMaximumValueViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("max value"); + rule.setFieldName("amount"); + rule.setNullAllowed(true); + rule.setDataType("Integer"); + rule.setMaximumValue("10"); + + ValidatorMeta meta = createMeta(rule); + RowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaInteger("amount")); + Validator validator = createInitializedValidator(meta, rowMeta, true); + + doReturn(new Object[] {11L}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("amount"), + eq("KVD006")); + } + + @Test + void testOnlyNullAllowedViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("only null"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setOnlyNullAllowed(true); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"x"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD015")); + } + + @Test + void testUnexpectedDataTypeViolation() throws Exception { + Validation rule = new Validation(); + rule.setName("type"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setDataType("Integer"); + rule.setDataTypeVerified(true); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"abc"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD004")); + } + + @Test + void testAllowedValueInListPasses() throws Exception { + Validation rule = new Validation(); + rule.setName("list"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setDataType("String"); + rule.setAllowedValues(List.of("A", "B")); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"A"}).doReturn(null).when(validator).getRow(); + doNothing().when(validator).putRow(any(IRowMeta.class), any(Object[].class)); + + assertTrue(validator.processRow()); + verify(validator).putRow(any(IRowMeta.class), eq(new Object[] {"A"})); + verify(validator, never()) + .putError( + any(IRowMeta.class), + any(Object[].class), + anyLong(), + anyString(), + anyString(), + anyString()); + } + + @Test + void testDisallowedRegularExpression() throws Exception { + Validation rule = new Validation(); + rule.setName("bad regex"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setRegularExpressionNotAllowed("^[0-9]+$"); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"123"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD014")); + } + + @Test + void testStartsWithNotAllowedString() throws Exception { + Validation rule = new Validation(); + rule.setName("bad start"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setStartStringNotAllowed("XX"); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"XXY"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD011")); + } + + @Test + void testEndsWithNotAllowedString() throws Exception { + Validation rule = new Validation(); + rule.setName("bad end"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setEndStringNotAllowed("YY"); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"aYY"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + eq("KVD012")); + } + + @Test + void testValidateAllWithoutConcatenateEmitsMultipleErrors() throws Exception { + Validation lengthRule = new Validation(); + lengthRule.setName("length"); + lengthRule.setFieldName("value"); + lengthRule.setNullAllowed(true); + lengthRule.setMaximumLength("1"); + + Validation onlyNull = new Validation(); + onlyNull.setName("only null"); + onlyNull.setFieldName("value"); + onlyNull.setNullAllowed(true); + onlyNull.setOnlyNullAllowed(true); + + ValidatorMeta meta = createMeta(lengthRule, onlyNull); + meta.setValidatingAll(true); + meta.setConcatenatingErrors(false); + + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + doReturn(new Object[] {"ab"}).when(validator).getRow(); + stubPutError(validator); + + assertTrue(validator.processRow()); + verify(validator, times(2)) + .putError( + any(IRowMeta.class), + any(Object[].class), + eq(1L), + anyString(), + eq("value"), + anyString()); + } + + @Test + void testNullInputEndsProcessing() throws Exception { + ValidatorMeta meta = createMeta(nullNotAllowed()); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(null).when(validator).getRow(); + + assertFalse(validator.processRow()); + } + + @Test + void testMissingFieldNameFailsInitCalculation() throws Exception { + Validation rule = new Validation(); + rule.setName("missing field"); + rule.setFieldName("does_not_exist"); + rule.setNullAllowed(true); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"x"}).when(validator).getRow(); + + HopException exception = assertThrows(HopException.class, validator::processRow); + assertTrue(exception.getMessage().contains("does_not_exist")); + } + + @Test + void testEmptyFieldNameFailsOnProcess() throws Exception { + Validation rule = new Validation(); + rule.setName("empty field name"); + rule.setFieldName(""); + rule.setNullAllowed(true); + + ValidatorMeta meta = createMeta(rule); + when(helper.transformMeta.isDoingErrorHandling()).thenReturn(true); + when(helper.pipelineMeta.getTransformFields(any(IVariables.class), anyString())) + .thenReturn(stringRowMeta()); + + ValidatorData data = new ValidatorData(); + Validator validator = + spy( + new Validator( + helper.transformMeta, meta, data, 0, helper.pipelineMeta, helper.pipeline)); + validator.setInputRowMeta(stringRowMeta()); + assertTrue(validator.init()); + + doReturn(new Object[] {"x"}).when(validator).getRow(); + + HopException exception = assertThrows(HopException.class, validator::processRow); + assertTrue(exception.getMessage().toLowerCase().contains("no name")); + } + + @Test + void testInitFailsForInvalidMinimumLength() throws Exception { + Validation rule = new Validation(); + rule.setName("bad length"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setMinimumLength("abc"); + + ValidatorMeta meta = createMeta(rule); + when(helper.pipelineMeta.getTransformFields(any(IVariables.class), anyString())) + .thenReturn(stringRowMeta()); + + Validator validator = + new Validator( + helper.transformMeta, + meta, + new ValidatorData(), + 0, + helper.pipelineMeta, + helper.pipeline); + + assertFalse(validator.init()); + } + + @Test + void testValidStartAndEndStringPasses() throws Exception { + Validation rule = new Validation(); + rule.setName("prefix-suffix"); + rule.setFieldName("value"); + rule.setNullAllowed(true); + rule.setStartString("AB"); + rule.setEndString("Z"); + + ValidatorMeta meta = createMeta(rule); + Validator validator = createInitializedValidator(meta, stringRowMeta(), true); + + doReturn(new Object[] {"ABZ"}).doReturn(null).when(validator).getRow(); + doNothing().when(validator).putRow(any(IRowMeta.class), any(Object[].class)); + + assertTrue(validator.processRow()); + verify(validator).putRow(any(IRowMeta.class), eq(new Object[] {"ABZ"})); + } + + private Validator createInitializedValidator( + ValidatorMeta meta, RowMeta rowMeta, boolean errorHandling) throws Exception { + when(helper.transformMeta.isDoingErrorHandling()).thenReturn(errorHandling); + when(helper.pipelineMeta.getTransformFields(any(IVariables.class), anyString())) + .thenReturn(rowMeta); + + ValidatorData data = new ValidatorData(); + Validator validator = + spy( + new Validator( + helper.transformMeta, meta, data, 0, helper.pipelineMeta, helper.pipeline)); + validator.setInputRowMeta(rowMeta); + + assertTrue(validator.init()); + return validator; + } + + private static void stubPutError(Validator validator) throws HopException { + doNothing() + .when(validator) + .putError( + any(IRowMeta.class), + any(Object[].class), + anyLong(), + anyString(), + nullable(String.class), + anyString()); + } + + private static ValidatorMeta createMeta(Validation... validations) { + ValidatorMeta meta = new ValidatorMeta(); + meta.setValidations(new ArrayList<>(List.of(validations))); + return meta; + } + + private static Validation nullNotAllowed() { + Validation rule = new Validation(); + rule.setName("value" + " null check"); + rule.setFieldName("value"); + rule.setNullAllowed(false); + return rule; + } + + private static RowMeta stringRowMeta() { + RowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("value")); + return rowMeta; + } +} diff --git a/plugins/transforms/validator/src/test/resources/validator-transform.xml b/plugins/transforms/validator/src/test/resources/validator-transform.xml new file mode 100644 index 00000000000..770f34184fc --- /dev/null +++ b/plugins/transforms/validator/src/test/resources/validator-transform.xml @@ -0,0 +1,60 @@ + + + + Data validator + Validator + + Y + + 1 + + none + + + Y + ; + Y + Y + + + A + B + + + String + N + + CODE_NULL + Field value cannot be null + + N + 10 + 1 + value + N + N + N + value rules + + + + 100 + 100 + +