Java tutorial
/* * Copyright (c) 2018, WSO2 Inc. (http://wso2.com) All Rights Reserved. * * Licensed 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.ballerinalang.langserver.command.testgen; import org.apache.commons.lang3.tuple.Pair; import org.ballerinalang.compiler.CompilerPhase; import org.ballerinalang.langserver.command.testgen.renderer.BLangPkgBasedRendererOutput; import org.ballerinalang.langserver.command.testgen.renderer.RendererOutput; import org.ballerinalang.langserver.command.testgen.renderer.TemplateBasedRendererOutput; import org.ballerinalang.langserver.command.testgen.template.RootTemplate; import org.ballerinalang.langserver.common.utils.CommonUtil; import org.ballerinalang.langserver.compiler.LSCompiler; import org.ballerinalang.langserver.compiler.LSCompilerException; import org.ballerinalang.langserver.compiler.common.modal.BallerinaFile; import org.ballerinalang.langserver.compiler.workspace.WorkspaceDocumentException; import org.ballerinalang.langserver.compiler.workspace.WorkspaceDocumentManager; import org.ballerinalang.model.elements.PackageID; import org.ballerinalang.model.tree.TopLevelNode; import org.eclipse.lsp4j.TextEdit; import org.wso2.ballerinalang.compiler.semantics.model.types.BInvokableType; import org.wso2.ballerinalang.compiler.semantics.model.types.BType; import org.wso2.ballerinalang.compiler.tree.BLangFunction; import org.wso2.ballerinalang.compiler.tree.BLangNode; import org.wso2.ballerinalang.compiler.tree.BLangPackage; import org.wso2.ballerinalang.compiler.tree.BLangService; import org.wso2.ballerinalang.compiler.tree.BLangSimpleVariable; import org.wso2.ballerinalang.compiler.tree.BLangVariable; import org.wso2.ballerinalang.compiler.tree.expressions.BLangExpression; import org.wso2.ballerinalang.compiler.tree.expressions.BLangServiceConstructorExpr; import org.wso2.ballerinalang.compiler.tree.expressions.BLangSimpleVarRef; import org.wso2.ballerinalang.compiler.tree.expressions.BLangTypeInit; import org.wso2.ballerinalang.compiler.tree.types.BLangFunctionTypeNode; import org.wso2.ballerinalang.compiler.tree.types.BLangType; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.StringJoiner; import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.ballerinalang.langserver.command.testgen.ValueSpaceGenerator.createTemplateArray; import static org.ballerinalang.langserver.command.testgen.ValueSpaceGenerator.getValueSpaceByNode; import static org.ballerinalang.langserver.command.testgen.ValueSpaceGenerator.getValueSpaceByType; import static org.ballerinalang.langserver.common.utils.CommonUtil.FunctionGenerator.generateTypeDefinition; /** * This class is responsible for generating tests for a given source file. * * @since 0.985.0 */ public class TestGenerator { private TestGenerator() { } /** * Creates a test file for a given BLangPackage in source file path. * * @param documentManager document manager * @param bLangNodePair A pair of {@link BLangNode} and fallback node * @param focusLineAcceptor focus line acceptor * @param builtSourceFile built {@link BLangPackage} source file * @param pkgRelativePath package relative path * @param testFile test file * @return generated test file path * @throws TestGeneratorException when test case generation fails */ public static List<TextEdit> generate(WorkspaceDocumentManager documentManager, Pair<BLangNode, Object> bLangNodePair, BiConsumer<Integer, Integer> focusLineAcceptor, BLangPackage builtSourceFile, String pkgRelativePath, File testFile) throws TestGeneratorException { RootTemplate template = getRootTemplate(pkgRelativePath, bLangNodePair, builtSourceFile, focusLineAcceptor); RendererOutput rendererOutput = getRendererOutput(documentManager, testFile, focusLineAcceptor); template.render(rendererOutput); return rendererOutput.getRenderedTextEdits(); } private static RendererOutput getRendererOutput(WorkspaceDocumentManager documentManager, File testFile, BiConsumer<Integer, Integer> focusLineAcceptor) throws TestGeneratorException { // If exists, read the test file content String testContent = ""; if (testFile.exists()) { try { // Reading through document manager since amendments are handled as text-edits testContent = documentManager.getFileContent(testFile.toPath()); } catch (WorkspaceDocumentException e) { throw new TestGeneratorException("Error occurred while compiling file path:" + testFile.toString(), e); } } // Create tests RendererOutput fileTemplate; if (testContent.isEmpty()) { // Create tests from file template fileTemplate = new TemplateBasedRendererOutput("rootTest.bal"); } else { // Create tests from blang package BallerinaFile ballerinaFile; try { ballerinaFile = LSCompiler.compileContent(testContent, CompilerPhase.COMPILER_PLUGIN); } catch (LSCompilerException e) { throw new TestGeneratorException("Could not compile the test content", e); } Optional<BLangPackage> optBLangPackage = ballerinaFile.getBLangPackage(); if (optBLangPackage.isPresent()) { fileTemplate = new BLangPkgBasedRendererOutput(optBLangPackage.get(), focusLineAcceptor); } else { String msg = "Appending failed! unknown error occurred while appending to:" + testFile.toString(); throw new TestGeneratorException(msg); } } return fileTemplate; } private static RootTemplate getRootTemplate(String fileName, Pair<BLangNode, Object> nodes, BLangPackage builtTestFile, BiConsumer<Integer, Integer> focusLineAcceptor) throws TestGeneratorException { BLangNode bLangNode = nodes.getLeft(); Object otherNode = nodes.getRight(); if (bLangNode == null && otherNode == null) { throw new TestGeneratorException("Target test construct not found!"); } if (bLangNode instanceof BLangFunction) { // A function return RootTemplate.fromFunction((BLangFunction) bLangNode, builtTestFile, focusLineAcceptor); } else if (bLangNode instanceof BLangService || hasServiceConstructor(bLangNode)) { // A Service BLangService service; if (bLangNode instanceof BLangService) { // is a service eg. service {}; service = (BLangService) bLangNode; } else { // is a service variable eg. service a = service {}; service = ((BLangServiceConstructorExpr) (((BLangSimpleVariable) bLangNode).expr)).serviceNode; } String owner = (service.listenerType != null) ? service.listenerType.tsymbol.owner.name.value : null; String serviceTypeName = (service.listenerType != null) ? service.listenerType.tsymbol.name.value : null; Optional<BLangTypeInit> optionalServiceInit = getServiceInit(builtTestFile, service); RootTemplate[] t = { null }; // Has ServiceInit optionalServiceInit.ifPresent(init -> { if ("http".equals(owner)) { switch (serviceTypeName) { case "Listener": t[0] = RootTemplate.fromHttpService(service, init, builtTestFile, focusLineAcceptor); break; case "WebSocketListener": t[0] = RootTemplate.fromHttpWSService(service, init, builtTestFile, focusLineAcceptor); break; default: // do nothing } } }); // Return service if (t[0] == null) { if (hasServiceConstructor(bLangNode)) { throw new TestGeneratorException("Services assigned to the variables are not supported!"); } throw new TestGeneratorException(owner + ":" + serviceTypeName + " services are not supported!"); } return t[0]; } // Whole file return new RootTemplate(fileName, builtTestFile, focusLineAcceptor); } private static boolean hasServiceConstructor(BLangNode bLangNode) { return (bLangNode instanceof BLangSimpleVariable && ((BLangSimpleVariable) bLangNode).expr instanceof BLangServiceConstructorExpr); } public static Optional<BLangTypeInit> getServiceInit(BLangPackage builtTestFile, BLangService service) { if (service.attachedExprs.isEmpty()) { return Optional.empty(); } BLangExpression expr = service.attachedExprs.get(0); if (expr instanceof BLangTypeInit) { // If in-line listener return Optional.of((BLangTypeInit) expr); } String[] variableName = { "" }; if (expr instanceof BLangSimpleVarRef) { // variable ref listener BLangSimpleVarRef varRef = (BLangSimpleVarRef) expr; variableName[0] = varRef.variableName.value; } for (TopLevelNode topLevelNode : builtTestFile.topLevelNodes) { if (topLevelNode instanceof BLangSimpleVariable) { BLangSimpleVariable var = (BLangSimpleVariable) topLevelNode; BLangExpression varExpr = var.expr; if (varExpr instanceof BLangTypeInit && variableName[0].equals(var.name.value)) { return Optional.of((BLangTypeInit) varExpr); } } } return Optional.empty(); } /** * This class provides functionalities for generating a test function for a given target function. */ public static class TestFunctionGenerator { public static final int VALUE_SPACE_LENGTH = 4; private String[][] valueSpace; private String[] typeSpace; private String[] namesSpace; private String functionName; private String returnType; public TestFunctionGenerator(BiConsumer<String, String> importsAcceptor, PackageID currentPkgId, BLangFunction function) { List<BLangSimpleVariable> params = function.requiredParams; List<BLangType> paramTypes = params.stream().map(variable -> variable.typeNode) .collect(Collectors.toList()); List<String> paramNames = new ArrayList<>(); params.forEach(variable -> paramNames.add(variable.name.value)); init(importsAcceptor, currentPkgId, function.name.value, paramNames, paramTypes, function.returnTypeNode); } public TestFunctionGenerator(BiConsumer<String, String> importsAcceptor, PackageID currentPkgId, BLangFunctionTypeNode type) { List<BLangVariable> params = type.params; List<BLangType> paramTypes = params.stream().map(variable -> variable.typeNode) .collect(Collectors.toList()); List<String> paramNames = new ArrayList<>(); params.forEach(variable -> { if (variable instanceof BLangSimpleVariable) { paramNames.add(((BLangSimpleVariable) variable).name.value); } else { paramNames.add(null); } }); init(importsAcceptor, currentPkgId, "", paramNames, paramTypes, type.returnTypeNode); } public TestFunctionGenerator(BiConsumer<String, String> importsAcceptor, PackageID currentPkgId, BInvokableType invokableType) { this.functionName = ""; List<BType> params = invokableType.paramTypes; BType returnBType = invokableType.retType; this.valueSpace = new String[VALUE_SPACE_LENGTH][params.size() + 1]; this.typeSpace = new String[params.size() + 1]; this.namesSpace = new String[params.size() + 1]; // Populate target function's parameters Set<String> lookupSet = new HashSet<>(); for (int i = 0; i < params.size(); i++) { String paramType = generateTypeDefinition(importsAcceptor, currentPkgId, params.get(i)); String paramName = CommonUtil.generateName(1, lookupSet); lookupSet.add(paramName); this.typeSpace[i] = paramType; this.namesSpace[i] = paramName; String[] pValueSpace = getValueSpaceByType(importsAcceptor, currentPkgId, params.get(i), createTemplateArray(VALUE_SPACE_LENGTH)); for (int j = 0; j < pValueSpace.length; j++) { // Need to apply transpose of `pValueSpace` // i.e. valueSpace = (pValueSpace)^T this.valueSpace[j][i] = pValueSpace[j]; } } // Populate target function's return type this.returnType = generateTypeDefinition(importsAcceptor, currentPkgId, returnBType); String[] rtValSpace = getValueSpaceByType(importsAcceptor, currentPkgId, returnBType, createTemplateArray(VALUE_SPACE_LENGTH)); this.typeSpace[params.size()] = returnType; this.namesSpace[params.size()] = "expected"; IntStream.range(0, rtValSpace.length).forEach(index -> { valueSpace[index][params.size()] = rtValSpace[index]; }); } private void init(BiConsumer<String, String> importsAcceptor, PackageID currentPkgId, String functionName, List<String> paramNames, List<BLangType> paramTypes, BLangType returnTypeNode) { this.functionName = functionName; this.valueSpace = new String[VALUE_SPACE_LENGTH][paramNames.size() + 1]; this.typeSpace = new String[paramNames.size() + 1]; this.namesSpace = new String[paramNames.size() + 1]; // Populate target function's parameters Set<String> lookupSet = new HashSet<>(); for (int i = 0; i < paramNames.size(); i++) { String paramType = generateTypeDefinition(importsAcceptor, currentPkgId, paramTypes.get(i)); String paramName = paramNames.get(i); if (paramName == null) { // If null, generate a param name paramName = CommonUtil.generateName(1, lookupSet); } this.typeSpace[i] = paramType; this.namesSpace[i] = paramName; String[] pValueSpace = getValueSpaceByNode(importsAcceptor, currentPkgId, paramTypes.get(i), createTemplateArray(VALUE_SPACE_LENGTH)); for (int j = 0; j < pValueSpace.length; j++) { // Need to apply transpose of `pValueSpace` // i.e. valueSpace = (pValueSpace)^T this.valueSpace[j][i] = pValueSpace[j]; } lookupSet.add(paramName); } // Populate target function's return type this.returnType = generateTypeDefinition(importsAcceptor, currentPkgId, returnTypeNode); String[] rtValSpace = getValueSpaceByNode(importsAcceptor, currentPkgId, returnTypeNode, createTemplateArray(VALUE_SPACE_LENGTH)); this.typeSpace[paramNames.size()] = returnType; this.namesSpace[paramNames.size()] = "expected"; IntStream.range(0, rtValSpace.length).forEach(index -> { valueSpace[index][paramNames.size()] = rtValSpace[index]; }); } /** * Returns parameters of the test function separated by comma. * <p> * eg. int x, int y * </p> * * @return string of type and name pairs */ public String getTestFuncParams() { StringJoiner paramsStr = new StringJoiner(", "); IntStream.range(0, this.namesSpace.length).forEach(index -> { String type = this.typeSpace[index]; String name = this.namesSpace[index]; paramsStr.add(type + " " + name); }); return paramsStr.toString(); } /** * Returns a list of target function invocations. * <p> * eg. ["foo(5, 20)","foo(100, 30)"] * </p> * * @return function invocation */ public List<String> getTargetFuncInvocations() { List<String> invocations = new ArrayList<>(); IntStream.range(0, this.valueSpace.length - 1).forEach(j -> { StringJoiner paramsInvokeStr = new StringJoiner(", "); IntStream.range(0, this.namesSpace.length - 1) .forEach(i -> paramsInvokeStr.add(this.valueSpace[j][i])); invocations.add(this.functionName + "(" + paramsInvokeStr.toString() + ")"); }); return invocations; } /** * Returns target function invocation string. * <p> * eg. foo(x, y) * </p> * * @return function invocation */ public String getTargetFuncInvocation() { StringJoiner paramsInvokeStr = new StringJoiner(", "); IntStream.range(0, this.namesSpace.length - 1).forEach(i -> paramsInvokeStr.add(this.namesSpace[i])); return this.functionName + "(" + paramsInvokeStr.toString() + ")"; } /** * Returns string of type of params of the data provider function. * <p> * eg. (int, float)[] * </p> * * @return return type */ public String getDataProviderReturnType() { StringJoiner paramsTypeStr = new StringJoiner(", "); IntStream.range(0, this.typeSpace.length).forEach(i -> paramsTypeStr.add(this.typeSpace[i])); return "(" + paramsTypeStr.toString() + ")[]"; } /** * Returns return type of the target function. * <p> * eg. (int, int) * </p> * * @return return type of the function */ public String getTargetFuncReturnType() { return this.returnType; } /** * Returns return value of the data provider function. * <p> * eg. [(1, 0.5),(-1, 2.5)] * </p> * * @return return value of the data provider */ public String getDataProviderReturnValue() { // Prepare data provider's return value StringJoiner vSpace = new StringJoiner("), (", "(", ")"); IntStream.range(0, valueSpace.length).forEach(index -> { vSpace.add(String.join(", ", valueSpace[index])); }); return "[" + vSpace.toString() + "]"; } /** * Returns the value space of the test function (including expected return value). * * @return value space */ public String[][] getValueSpace() { return valueSpace.clone(); } /** * Returns the types space of the test function (including expected return type). * * @return type space */ public String[] getTypeSpace() { return typeSpace.clone(); } /** * Returns the names space of the test function (including expected return param name). * * @return names space */ public String[] getNamesSpace() { return namesSpace.clone(); } } }