/* * Copyright 2015 Nokia Solutions and Networks * Licensed under the Apache License, Version 2.0, * see license.txt file for details. */ package org.robotframework.ide.eclipse.main.plugin.project.build.validation; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Sets.newHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.rf.ide.core.testdata.model.RobotFileOutput.BuildMessage; import org.rf.ide.core.testdata.model.search.keyword.KeywordScope; import org.rf.ide.core.testdata.model.table.RobotExecutableRow; import org.rf.ide.core.testdata.model.table.TestCaseTable; import org.rf.ide.core.testdata.model.table.exec.descs.IExecutableRowDescriptor; import org.rf.ide.core.testdata.model.table.exec.descs.IExecutableRowDescriptor.ERowType; import org.rf.ide.core.testdata.model.table.exec.descs.RobotAction; import org.rf.ide.core.testdata.model.table.exec.descs.VariableExtractor; import org.rf.ide.core.testdata.model.table.exec.descs.ast.mapping.MappingResult; import org.rf.ide.core.testdata.model.table.exec.descs.ast.mapping.VariableDeclaration; import org.rf.ide.core.testdata.model.table.exec.descs.impl.ForLoopContinueRowDescriptor; import org.rf.ide.core.testdata.model.table.keywords.names.GherkinStyleSupport; import org.rf.ide.core.testdata.model.table.keywords.names.GherkinStyleSupport.NameTransformation; import org.rf.ide.core.testdata.model.table.keywords.names.QualifiedKeywordName; import org.rf.ide.core.testdata.model.table.testcases.TestCase; import org.rf.ide.core.testdata.model.table.testcases.TestCaseSetup; import org.rf.ide.core.testdata.model.table.testcases.TestCaseTags; import org.rf.ide.core.testdata.model.table.testcases.TestCaseTeardown; import org.rf.ide.core.testdata.model.table.testcases.TestCaseTimeout; import org.rf.ide.core.testdata.model.table.variables.names.VariableNamesSupport; import org.rf.ide.core.testdata.text.read.recognizer.RobotToken; import org.rf.ide.core.validation.ProblemPosition; import org.robotframework.ide.eclipse.main.plugin.model.RobotCase; import org.robotframework.ide.eclipse.main.plugin.model.RobotCasesSection; import org.robotframework.ide.eclipse.main.plugin.model.locators.KeywordEntity; import org.robotframework.ide.eclipse.main.plugin.project.build.AdditionalMarkerAttributes; import org.robotframework.ide.eclipse.main.plugin.project.build.AttributesAugmentingReportingStrategy; import org.robotframework.ide.eclipse.main.plugin.project.build.ProblemsReportingStrategy; import org.robotframework.ide.eclipse.main.plugin.project.build.RobotArtifactsValidator.ModelUnitValidator; import org.robotframework.ide.eclipse.main.plugin.project.build.RobotProblem; import org.robotframework.ide.eclipse.main.plugin.project.build.causes.ArgumentProblem; import org.robotframework.ide.eclipse.main.plugin.project.build.causes.GeneralSettingsProblem; import org.robotframework.ide.eclipse.main.plugin.project.build.causes.KeywordsProblem; import org.robotframework.ide.eclipse.main.plugin.project.build.causes.TestCasesProblem; import org.robotframework.ide.eclipse.main.plugin.project.build.validation.FileValidationContext.ValidationKeywordEntity; import org.robotframework.ide.eclipse.main.plugin.project.build.validation.testcases.DocumentationTestCaseDeclarationSettingValidator; import org.robotframework.ide.eclipse.main.plugin.project.build.validation.testcases.PostconditionDeclarationExistenceValidator; import org.robotframework.ide.eclipse.main.plugin.project.build.validation.testcases.PreconditionDeclarationExistenceValidator; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Range; class TestCaseTableValidator implements ModelUnitValidator { private final FileValidationContext validationContext; private final Optional<RobotCasesSection> testCaseSection; private final ProblemsReportingStrategy reporter; TestCaseTableValidator(final FileValidationContext validationContext, final Optional<RobotCasesSection> section, final ProblemsReportingStrategy reporter) { this.validationContext = validationContext; this.testCaseSection = section; this.reporter = reporter; } @Override public void validate(final IProgressMonitor monitor) throws CoreException { if (!testCaseSection.isPresent()) { return; } final RobotCasesSection robotCasesSection = testCaseSection.get(); final TestCaseTable casesTable = robotCasesSection.getLinkedElement(); final List<TestCase> cases = casesTable.getTestCases(); validateByExternal(robotCasesSection, monitor); reportEmptyNamesOfCases(cases); reportEmptyCases(cases); reportDuplicatedCases(cases); reportSettingsProblems(cases); reportKeywordUsageProblems(robotCasesSection.getChildren()); reportUnknownVariables(cases); } private void validateByExternal(final RobotCasesSection section, final IProgressMonitor monitor) throws CoreException { new DocumentationTestCaseDeclarationSettingValidator(validationContext.getFile(), section, reporter) .validate(monitor); new PreconditionDeclarationExistenceValidator(validationContext.getFile(), reporter, section).validate(monitor); new PostconditionDeclarationExistenceValidator(validationContext.getFile(), reporter, section) .validate(monitor); } private void reportEmptyNamesOfCases(final List<TestCase> cases) { for (final TestCase testCase : cases) { final RobotToken caseName = testCase.getName(); if (caseName.getText().trim().isEmpty()) { final RobotProblem problem = RobotProblem.causedBy(TestCasesProblem.EMPTY_CASE_NAME); final int startOffset = caseName.getStartOffset(); final int endOffset = caseName.getEndOffset(); final ProblemPosition problemPosition = new ProblemPosition(caseName.getFilePosition().getLine(), Range.closed(startOffset, endOffset)); reporter.handleProblem(problem, validationContext.getFile(), problemPosition); } } } private void reportEmptyCases(final List<TestCase> cases) { for (final TestCase testCase : cases) { final RobotToken caseName = testCase.getTestName(); if (!hasAnythingToExecute(testCase)) { final String name = caseName.getText(); final RobotProblem problem = RobotProblem.causedBy(TestCasesProblem.EMPTY_CASE).formatMessageWith(name); final Map<String, Object> arguments = ImmutableMap.<String, Object> of(AdditionalMarkerAttributes.NAME, name); reporter.handleProblem(problem, validationContext.getFile(), caseName, arguments); } } } private boolean hasAnythingToExecute(final TestCase testCase) { for (final RobotExecutableRow<?> robotExecutableRow : testCase.getTestExecutionRows()) { if (robotExecutableRow.isExecutable()) { return true; } } return false; } private void reportDuplicatedCases(final List<TestCase> cases) { final Set<String> duplicatedNames = newHashSet(); for (final TestCase case1 : cases) { for (final TestCase case2 : cases) { if (case1 != case2) { final String case1Name = case1.getTestName().getText(); final String case2Name = case2.getTestName().getText(); if (case1Name.equalsIgnoreCase(case2Name)) { duplicatedNames.add(case1Name.toLowerCase()); } } } } for (final TestCase testCase : cases) { final RobotToken caseName = testCase.getTestName(); final String name = caseName.getText(); if (duplicatedNames.contains(name.toLowerCase())) { final RobotProblem problem = RobotProblem.causedBy(TestCasesProblem.DUPLICATED_CASE) .formatMessageWith(name); final Map<String, Object> additionalArguments = ImmutableMap.<String, Object> of("name", name); reporter.handleProblem(problem, validationContext.getFile(), caseName, additionalArguments); } } } private void reportSettingsProblems(final List<TestCase> cases) throws CoreException { for (final TestCase testCase : cases) { new TestCaseSettingsValidator(validationContext, testCase, reporter).validate(null); } } void reportKeywordUsageProblems(final List<RobotCase> cases) { for (final RobotCase testCase : cases) { reportKeywordUsageProblemsInTestCaseSettings(testCase); reportKeywordUsageProblems(validationContext, reporter, testCase.getLinkedElement().getTestExecutionRows(), testCase.getTemplateInUse()); } } private void reportKeywordUsageProblemsInTestCaseSettings(final RobotCase testCase) { final RobotToken templateKeywordToken = testCase.getLinkedElement().getTemplateKeywordLocation(); if (templateKeywordToken != null && !templateKeywordToken.getFilePosition().isNotSet() && isTemplateFromTestCasesTable(testCase) && !templateKeywordToken.getText().toLowerCase().equals("none")) { validateExistingKeywordCall(validationContext, reporter, templateKeywordToken, Optional.<List<RobotToken>> empty()); } } static void reportKeywordUsageProblemsInSetupAndTeardownSetting(final FileValidationContext validationContext, final ProblemsReportingStrategy reporter, final RobotToken keywordNameToken, final Optional<List<RobotToken>> arguments) { final MappingResult variablesExtraction = new VariableExtractor().extract(keywordNameToken, validationContext.getFile().getName()); final List<VariableDeclaration> variablesDeclarations = variablesExtraction.getCorrectVariables(); if (variablesExtraction.getMappedElements().size() == 1 && variablesDeclarations.size() == 1) { final RobotProblem problem = RobotProblem .causedBy(GeneralSettingsProblem.VARIABLE_AS_KEYWORD_USAGE_IN_SETTING) .formatMessageWith(variablesDeclarations.get(0).getVariableName().getText()); reporter.handleProblem(problem, validationContext.getFile(), keywordNameToken); } else { validateExistingKeywordCall(validationContext, reporter, keywordNameToken, arguments); } } static void reportKeywordUsageProblems(final FileValidationContext validationContext, final ProblemsReportingStrategy reporter, final List<? extends RobotExecutableRow<?>> executables, final Optional<String> templateKeyword) { for (final RobotExecutableRow<?> executable : executables) { if (!executable.isExecutable() || templateKeyword.isPresent()) { continue; } final IExecutableRowDescriptor<?> executableRowDescriptor = executable.buildLineDescription(); RobotToken keywordName = executableRowDescriptor.getAction().getToken(); final IFile file = validationContext.getFile(); if (executableRowDescriptor.getRowType() == ERowType.FOR) { final List<BuildMessage> messages = executableRowDescriptor.getMessages(); for (final BuildMessage buildMessage : messages) { final RobotProblem problem = RobotProblem.causedBy(KeywordsProblem.INVALID_FOR_KEYWORD) .formatMessageWith(buildMessage.getMessage()); final ProblemPosition position = new ProblemPosition(buildMessage.getFileRegion().getStart().getLine(), Range.closed(buildMessage.getFileRegion().getStart().getOffset(), buildMessage.getFileRegion().getEnd().getOffset())); reporter.handleProblem(problem, file, position); } continue; } if (executableRowDescriptor.getRowType() == ERowType.FOR_CONTINUE) { final ForLoopContinueRowDescriptor<?> loopContinueRowDescriptor = (ForLoopContinueRowDescriptor<?>) executable .buildLineDescription(); keywordName = loopContinueRowDescriptor.getKeywordAction().getToken(); } if (!keywordName.getFilePosition().isNotSet()) { validateExistingKeywordCall(validationContext, reporter, keywordName, Optional.of(executableRowDescriptor.getKeywordArguments())); } else { reporter.handleProblem(RobotProblem.causedBy(KeywordsProblem.MISSING_KEYWORD) .formatMessageWith(executable.getAction().getText()), file, executable.getAction()); } } } static void validateExistingKeywordCall(final FileValidationContext validationContext, final ProblemsReportingStrategy reporter, final RobotToken keywordName, final Optional<List<RobotToken>> arguments) { final ListMultimap<String, KeywordEntity> keywordProposal = validationContext .findPossibleKeywords(keywordName.getText()); final Optional<String> nameToUse = GherkinStyleSupport.firstNameTransformationResult(keywordName.getText(), new NameTransformation<String>() { @Override public Optional<String> transform(final String gherkinNameVariant) { return validationContext.isKeywordAccessible(keywordProposal, gherkinNameVariant) ? Optional.of(gherkinNameVariant) : Optional.<String> empty(); } }); final String name = !nameToUse.isPresent() || nameToUse.get().isEmpty() ? keywordName.getText() : nameToUse.get(); final int offset = keywordName.getStartOffset() + (keywordName.getText().length() - name.length()); final ProblemPosition position = new ProblemPosition(keywordName.getLineNumber(), Range.closed(offset, offset + name.length())); if (!nameToUse.isPresent()) { reporter.handleProblem(RobotProblem.causedBy(KeywordsProblem.UNKNOWN_KEYWORD).formatMessageWith(name), validationContext.getFile(), position, ImmutableMap.<String, Object> of(AdditionalMarkerAttributes.NAME, name, AdditionalMarkerAttributes.ORIGINAL_NAME, keywordName.getText())); return; } final ListMultimap<KeywordScope, KeywordEntity> keywords = validationContext .getPossibleKeywords(keywordProposal, name); for (final KeywordScope scope : KeywordScope.defaultOrder()) { final List<KeywordEntity> keywordEntities = keywords.get(scope); if (keywordEntities.size() == 1) { final ValidationKeywordEntity keyword = (ValidationKeywordEntity) keywordEntities.get(0); if (keyword.isDeprecated()) { reporter.handleProblem( RobotProblem.causedBy(KeywordsProblem.DEPRECATED_KEYWORD).formatMessageWith(name), validationContext.getFile(), position); } if (keyword.isFromNestedLibrary(validationContext.getFile())) { reporter.handleProblem( RobotProblem.causedBy(KeywordsProblem.KEYWORD_FROM_NESTED_LIBRARY).formatMessageWith(name), validationContext.getFile(), position); } if (keyword.hasInconsistentName(name)) { reporter.handleProblem( RobotProblem.causedBy(KeywordsProblem.KEYWORD_OCCURRENCE_NOT_CONSISTENT_WITH_DEFINITION) .formatMessageWith(name, keyword.getNameFromDefinition()), validationContext.getFile(), position, ImmutableMap.<String, Object> of(AdditionalMarkerAttributes.NAME, name, AdditionalMarkerAttributes.ORIGINAL_NAME, keyword.getNameFromDefinition(), AdditionalMarkerAttributes.SOURCES, keyword.getSourceNameInUse())); } if (arguments.isPresent()) { new KeywordCallArgumentsValidator(validationContext.getFile(), keywordName, reporter, keyword.getArgumentsDescriptor(), arguments.get()).validate(new NullProgressMonitor()); } break; } else if (keywordEntities.size() > 1) { final Iterable<?> sources = transform(keywordEntities, new Function<KeywordEntity, String>() { @Override public String apply(final KeywordEntity kw) { return kw.getSourceNameInUse(); } }); reporter.handleProblem( RobotProblem.causedBy(KeywordsProblem.AMBIGUOUS_KEYWORD).formatMessageWith(name, "[" + Joiner.on(", ").join(sources) + "]"), validationContext.getFile(), position, ImmutableMap.<String, Object> of(AdditionalMarkerAttributes.NAME, name, AdditionalMarkerAttributes.ORIGINAL_NAME, keywordName.getText(), AdditionalMarkerAttributes.SOURCES, Joiner.on(';').join(sources))); break; } } } private void reportUnknownVariables(final List<TestCase> cases) { final Set<String> variables = validationContext.getAccessibleVariables(); for (final TestCase testCase : cases) { reportUnknownVariablesInTimeoutSetting(testCase, variables); reportUnknownVariablesInTags(testCase, variables); reportUnknownVariables(validationContext, reporter, collectTestCaseExeRowsForVariablesChecking(testCase), variables); } } private List<? extends RobotExecutableRow<?>> collectTestCaseExeRowsForVariablesChecking(final TestCase testCase) { final List<RobotExecutableRow<?>> exeRows = newArrayList(); final List<TestCaseSetup> setups = testCase.getSetups(); if (!setups.isEmpty()) { exeRows.add(setups.get(0).asExecutableRow()); } exeRows.addAll(testCase.getTestExecutionRows()); final List<TestCaseTeardown> teardowns = testCase.getTeardowns(); if (!teardowns.isEmpty()) { exeRows.add(teardowns.get(0).asExecutableRow()); } return exeRows; } private void reportUnknownVariablesInTimeoutSetting(final TestCase testCase, final Set<String> variables) { final List<TestCaseTimeout> timeouts = testCase.getTimeouts(); for (final TestCaseTimeout testCaseTimeout : timeouts) { final RobotToken timeoutToken = testCaseTimeout.getTimeout(); if (timeoutToken != null) { validateTimeoutSetting(validationContext, reporter, variables, timeoutToken); } } } static void validateTimeoutSetting(final FileValidationContext validationContext, final ProblemsReportingStrategy reporter, final Set<String> variables, final RobotToken timeoutToken) { final String timeout = timeoutToken.getText(); if (!RobotTimeFormat.isValidRobotTimeArgument(timeout.trim())) { if (containsVariables(validationContext, timeoutToken)) { final UnknownVariables unknownVarsValidator = new UnknownVariables(validationContext, reporter); unknownVarsValidator.reportUnknownVars(newArrayList(timeoutToken), variables); } else { final RobotProblem problem = RobotProblem.causedBy(ArgumentProblem.INVALID_TIME_FORMAT) .formatMessageWith(timeout); reporter.handleProblem(problem, validationContext.getFile(), timeoutToken); } } } private static boolean containsVariables(final FileValidationContext validationContext, final RobotToken timeoutToken) { final String filename = validationContext.getFile().getName(); return !new VariableExtractor().extract(timeoutToken, filename).getCorrectVariables().isEmpty(); } private void reportUnknownVariablesInTags(final TestCase testCase, final Set<String> variables) { final List<TestCaseTags> tags = testCase.getTags(); final UnknownVariables unknownVarsValidator = new UnknownVariables(validationContext, reporter); for (final TestCaseTags tag : tags) { unknownVarsValidator.reportUnknownVars(tag.getTags(), variables); } } static void reportUnknownVariables(final FileValidationContext validationContext, final ProblemsReportingStrategy reporter, final List<? extends RobotExecutableRow<?>> executables, final Set<String> variables) { final Set<String> definedVariables = newHashSet(variables); final Map<String, Object> additionalMarkerAttributes = ImmutableMap .<String, Object> of(AdditionalMarkerAttributes.DEFINE_VAR_LOCALLY, Boolean.TRUE); final ProblemsReportingStrategy reportingStrategy = AttributesAugmentingReportingStrategy .create(reporter, additionalMarkerAttributes); final UnknownVariables unknownVarsValidator = new UnknownVariables(validationContext, reportingStrategy); for (final RobotExecutableRow<?> row : executables) { if (row.isExecutable()) { final IExecutableRowDescriptor<?> lineDescription = row.buildLineDescription(); final Predicate<VariableDeclaration> isInvalid = new Predicate<VariableDeclaration>() { @Override public boolean apply(final VariableDeclaration variableDeclaration) { return isInvalidVariableDeclaration(validationContext, definedVariables, lineDescription, variableDeclaration); } }; unknownVarsValidator.reportUnknownVariables(lineDescription.getUsedVariables(), isInvalid); definedVariables.addAll( VariableNamesSupport.extractUnifiedVariableNames(lineDescription.getCreatedVariables())); } } } static boolean isInvalidVariableDeclaration(final FileValidationContext validationContext, final Set<String> variables, final IExecutableRowDescriptor<?> lineDescription, final VariableDeclaration declaration) { return UnknownVariables.isInvalidVariableDeclaration(variables, declaration) && !isVariableInSetterOrGetterOrCommentKeyword(validationContext, variables, lineDescription, declaration); } private static boolean isVariableInSetterOrGetterOrCommentKeyword(final FileValidationContext validationContext, final Set<String> definedVariables, final IExecutableRowDescriptor<?> lineDescription, final VariableDeclaration variableDeclaration) { final String keywordName = QualifiedKeywordName.fromOccurrence(getKeyword(lineDescription)).getKeywordName(); if (keywordName.equals("settestvariable") && isKeywordFromBuiltin(validationContext, keywordName)) { final List<VariableDeclaration> usedVariables = lineDescription.getUsedVariables(); if (!usedVariables.isEmpty()) { definedVariables .add(VariableNamesSupport.extractUnifiedVariableName(usedVariables.get(0).asToken().getText())); return true; } } else if (keywordName.equals("getvariablevalue") && isKeywordFromBuiltin(validationContext, keywordName)) { final List<VariableDeclaration> usedVariables = lineDescription.getUsedVariables(); if (!usedVariables.isEmpty() && usedVariables.get(0).equals(variableDeclaration)) { return true; } } else if (keywordName.equals("comment") && isKeywordFromBuiltin(validationContext, keywordName)) { return true; } return false; } private static boolean isKeywordFromBuiltin(final FileValidationContext validationContext, final String keywordName) { for (final KeywordScope scope : KeywordScope.defaultOrder()) { final List<KeywordEntity> possible = validationContext.getPossibleKeywords(keywordName, true).get(scope); if (scope != KeywordScope.STD_LIBRARY && !possible.isEmpty()) { return false; } else if (scope == KeywordScope.STD_LIBRARY) { return possible.size() == 1 && possible.get(0).getSourceNameInUse().equals("BuiltIn"); } } return false; } private static String getKeyword(final IExecutableRowDescriptor<?> lineDescription) { final RobotAction action; if (lineDescription.getRowType() == ERowType.FOR_CONTINUE) { action = ((ForLoopContinueRowDescriptor<?>) lineDescription).getKeywordAction(); } else { action = lineDescription.getAction(); } return action.getToken().getText(); } private boolean isTemplateFromTestCasesTable(final RobotCase testCase) { return testCase.getLinkedElement().getRobotViewAboutTestTemplate() != null; } }