├── src ├── main │ ├── resources │ │ ├── ManifestFile_For_Unit_Test_Classes.txt │ │ ├── ClassManifestFile.txt │ │ ├── ManifestFile_For_Functional_Test_Classes.txt │ │ ├── ManifestFile_For_Integration_Test_Classes.txt │ │ ├── ManifestFile.txt │ │ ├── config.properties │ │ └── logback.xml │ └── java │ │ └── com │ │ └── sforce │ │ └── cd │ │ └── apexUnit │ │ ├── client │ │ ├── connection │ │ │ ├── ConnectorConfigInterface.java │ │ │ ├── CommonConnectorConfig.java │ │ │ ├── SFDCSessionRenewer.java │ │ │ ├── BulkConnectionConnectorConfig.java │ │ │ ├── PartnerConnectionConnectorConfig.java │ │ │ └── ConnectionHandler.java │ │ ├── codeCoverage │ │ │ ├── CodeCoverageTask.java │ │ │ ├── OAuthTokenGenerator.java │ │ │ ├── WebServiceInvoker.java │ │ │ └── CodeCoverageComputer.java │ │ ├── testEngine │ │ │ ├── TestExecutor.java │ │ │ ├── TestStatusPollerAndResultHandler.java │ │ │ └── AsyncBulkApiHandler.java │ │ ├── fileReader │ │ │ └── ApexManifestFileReader.java │ │ └── QueryConstructor.java │ │ ├── report │ │ ├── ApexUnitCodeCoverageResults.java │ │ ├── ApexMethodCodeCoverageBean.java │ │ ├── ApexReportBean.java │ │ ├── ApexClassCodeCoverageBean.java │ │ ├── ApexUnitTestReportGenerator.java │ │ └── ApexCodeCoverageReportGenerator.java │ │ ├── ApexUnitUtils.java │ │ ├── arguments │ │ ├── PositiveIntegerValidator.java │ │ ├── PercentageInputValidator.java │ │ ├── URLValidator.java │ │ └── CommandLineArguments.java │ │ └── ApexUnitRunner.java └── test │ └── java │ └── com │ └── sforce │ └── cd │ └── apexUnit │ ├── testEngine │ └── TestExecutorTest.java │ ├── client │ ├── OAuthTokenGeneratorTest.java │ ├── ConnectionHandlerTest.java │ ├── PartnerConnectionConnectorConfigTest.java │ ├── ToolingAPIInvokerTest.java │ ├── ApexClassFetcherUtilsTest.java │ └── AsyncBulkApiHandlerTest.java │ ├── arguments │ ├── URLValidatorTest.java │ ├── PercentageInputValidatorTest.java │ └── CommandLineArgumentsTest.java │ ├── report │ └── ApexReportGeneratorTest.java │ └── fileReader │ └── ApexManifestFileReaderTest.java ├── Slide Deck.pdf ├── CODEOWNERS ├── .gitignore ├── .travis.yml ├── license.txt ├── pom.xml └── README.md /src/main/resources/ManifestFile_For_Unit_Test_Classes.txt: -------------------------------------------------------------------------------- 1 | test1test -------------------------------------------------------------------------------- /Slide Deck.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/ApexUnit/HEAD/Slide Deck.pdf -------------------------------------------------------------------------------- /src/main/resources/ClassManifestFile.txt: -------------------------------------------------------------------------------- 1 | AuthorPageConfig 2 | WrapperExampleController -------------------------------------------------------------------------------- /src/main/resources/ManifestFile_For_Functional_Test_Classes.txt: -------------------------------------------------------------------------------- 1 | AuthorPageConfig 2 | AuthorPageConfigTest 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/ManifestFile_For_Integration_Test_Classes.txt: -------------------------------------------------------------------------------- 1 | AuthorPageConfig 2 | AuthorPageConfigTest 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/ManifestFile.txt: -------------------------------------------------------------------------------- 1 | SimpleClass1 2 | AuthorPageConfig 3 | AuthorPageConfigTest 4 | SampleClass2 5 | 6 | HelloWorldTrigger -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.DS_Store 3 | .DS_Store 4 | 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | /target 16 | 17 | 18 | .classpath 19 | 20 | .project 21 | 22 | *.prefs 23 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/ConnectorConfigInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client.connection; 8 | 9 | import com.sforce.ws.ConnectorConfig; 10 | 11 | public interface ConnectorConfigInterface { 12 | ConnectorConfig createConfig(); 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk7 5 | 6 | script: 7 | - mvn test -D-org.login.url="https://na14.salesforce.com" -D-org.username=$USERNAME -D-org.password=$PASSWORD -D-org.client.id=$CLIENT_ID -D-org.client.secret=$CLIENT_SECRET -D-org.wide.code.coverage.threshold=40 -D-team.code.coverage.threshold=40 -D-regex.for.selecting.source.classes.for.code.coverage.computation=HelloWorld -D-regex.for.selecting.test.classes.to.execute=HelloWord* -D-manifest.files.with.test.class.names.to.execute=ManifestFile_For_Unit_Test_Classes.txt -D-manifest.files.with.source.class.names.for.code.coverage.computation=ManifestFile.txt -D-max.test.execution.time.threshold=10 8 | 9 | env: 10 | global: 11 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexUnitCodeCoverageResults.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class that stores the code coverage computation results 10 | * @author adarsh.ramakrishna@salesforce.com 11 | */ 12 | 13 | 14 | package com.sforce.cd.apexUnit.report; 15 | 16 | /* 17 | * class to store the results of the code coverage for the Apex classes 18 | */ 19 | public class ApexUnitCodeCoverageResults { 20 | public static double teamCodeCoverage = -1.0; 21 | public static double orgWideCodeCoverage = -1.0; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/codeCoverage/CodeCoverageTask.java: -------------------------------------------------------------------------------- 1 | package com.sforce.cd.apexUnit.client.codeCoverage; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import org.json.simple.JSONObject; 6 | public class CodeCoverageTask implements Callable{ 7 | 8 | private final String relativeUrl; 9 | private final String soqlcc; 10 | private final String oauthTocken; 11 | 12 | public CodeCoverageTask(String relativeUrl, String soqlcc, String oauthTocken){ 13 | this.relativeUrl = relativeUrl; 14 | this.soqlcc = soqlcc; 15 | this.oauthTocken = oauthTocken; 16 | } 17 | 18 | 19 | public JSONObject call() throws Exception { 20 | 21 | return WebServiceInvoker.doGet(relativeUrl, soqlcc, oauthTocken); 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/testEngine/TestExecutorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | package com.sforce.cd.apexUnit.testEngine; 12 | 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.testng.annotations.BeforeTest; 16 | import org.testng.annotations.Test; 17 | 18 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 19 | import com.sforce.cd.apexUnit.client.testEngine.TestExecutor; 20 | import com.sforce.soap.partner.PartnerConnection; 21 | 22 | public class TestExecutorTest { 23 | 24 | private static final Logger LOG = LoggerFactory.getLogger(TestExecutorTest.class); 25 | PartnerConnection conn = null; 26 | 27 | @BeforeTest 28 | public void setup() { 29 | ConnectionHandler connectionHandler = ConnectionHandler.getConnectionHandlerInstance(); 30 | //conn = connectionHandler.getConnection(); 31 | } 32 | 33 | // @Test 34 | public void testLogicalFlow() { 35 | TestExecutor flowController = new TestExecutor(); 36 | // flowController.logicalFlow(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/CommonConnectorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client.connection; 8 | 9 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 10 | import com.sforce.ws.ConnectorConfig; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class CommonConnectorConfig implements ConnectorConfigInterface { 15 | private static Logger LOG = LoggerFactory.getLogger(CommonConnectorConfig.class); 16 | 17 | public ConnectorConfig createConfig() { 18 | ConnectorConfig config = new ConnectorConfig(); 19 | config.setUsername(CommandLineArguments.getUsername()); 20 | config.setPassword(CommandLineArguments.getPassword()); 21 | config.setCompression(true); 22 | if (CommandLineArguments.getProxyHost() != null && CommandLineArguments.getProxyPort() != null) { 23 | LOG.debug("Setting proxy configuraiton to " + CommandLineArguments.getProxyHost() + " on port " 24 | + CommandLineArguments.getProxyPort()); 25 | config.setProxy(CommandLineArguments.getProxyHost(), CommandLineArguments.getProxyPort()); 26 | } 27 | return config; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/SFDCSessionRenewer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client.connection; 8 | 9 | import javax.xml.namespace.QName; 10 | 11 | import com.sforce.soap.partner.PartnerConnection; 12 | import com.sforce.ws.ConnectionException; 13 | import com.sforce.ws.ConnectorConfig; 14 | import com.sforce.ws.SessionRenewer; 15 | 16 | public class SFDCSessionRenewer implements SessionRenewer { 17 | // Thanks to Thys Michels blog for an example implementation. Reference: 18 | // http://thysmichels.com/2014/02/15/salesforce-wsc-partner-connection-session-renew-when-session-timeout/ 19 | public SessionRenewalHeader renewSession(ConnectorConfig config) throws ConnectionException { 20 | PartnerConnection connection = ConnectionHandler.getConnectionHandlerInstance().getConnection(); 21 | SessionRenewalHeader sessionRenewalHeader = new SessionRenewalHeader(); 22 | sessionRenewalHeader.name = new QName("urn:partner.soap.sforce.com", "SessionHeader"); 23 | if(connection != null ){ 24 | sessionRenewalHeader.headerElement = connection.getSessionHeader(); 25 | } 26 | return sessionRenewalHeader; 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/BulkConnectionConnectorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client.connection; 8 | 9 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 10 | import com.sforce.ws.ConnectorConfig; 11 | 12 | public class BulkConnectionConnectorConfig implements ConnectorConfigInterface { 13 | 14 | public ConnectorConfig createConfig() { 15 | // When PartnerConnection is instantiated, a login is implicitly 16 | // executed and, if successful, 17 | // a valid session is stored in the ConnectorConfig instance. 18 | // Use this key to initialize a BulkConnection: 19 | CommonConnectorConfig commonConnConfig = new CommonConnectorConfig(); 20 | ConnectorConfig config = commonConnConfig.createConfig(); 21 | String sessionId = ConnectionHandler.getConnectionHandlerInstance().getSessionIdFromConnectorConfig(); 22 | config.setSessionId(sessionId); 23 | String restEndPoint = CommandLineArguments.getOrgUrl() + "/services/async/" 24 | + ConnectionHandler.SUPPORTED_VERSION; 25 | config.setRestEndpoint(restEndPoint); 26 | config.setTraceMessage(false); 27 | return config; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/PartnerConnectionConnectorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client.connection; 8 | 9 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 10 | import com.sforce.ws.ConnectorConfig; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class PartnerConnectionConnectorConfig implements ConnectorConfigInterface { 15 | private static Logger LOG = LoggerFactory.getLogger(PartnerConnectionConnectorConfig.class); 16 | 17 | public ConnectorConfig createConfig() { 18 | CommonConnectorConfig commonConnConfig = new CommonConnectorConfig(); 19 | ConnectorConfig config = commonConnConfig.createConfig(); 20 | config.setAuthEndpoint( 21 | CommandLineArguments.getOrgUrl() + "/services/Soap/u/" + ConnectionHandler.SUPPORTED_VERSION); 22 | config.setSessionRenewer(new SFDCSessionRenewer()); 23 | LOG.info("Default connection time out value is: " + config.getConnectionTimeout()); 24 | config.setConnectionTimeout(ConnectionHandler.MAX_TIME_OUT_IN_MS_INT); 25 | LOG.info("Updated connection time out value(from config.properties file): " + config.getConnectionTimeout()); 26 | return config; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/ApexUnitUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * ApexUnitUtils class consists of utility methods used by the ApexUnit tool like the shutdown logic 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | package com.sforce.cd.apexUnit; 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | public class ApexUnitUtils { 19 | 20 | 21 | private static Logger LOG = LoggerFactory 22 | .getLogger(ApexUnitUtils.class); 23 | 24 | 25 | /** 26 | * Shutdown framework with an error message and print stack trace 27 | * @param ex Exception thrown by the application 28 | */ 29 | public static void shutDownWithDebugLog(Exception ex, String errorMsg){ 30 | if(LOG.isDebugEnabled()) { 31 | ex.printStackTrace(); 32 | } 33 | shutDownWithErrMsg(errorMsg); 34 | } 35 | 36 | /** 37 | * Shutdown framework with an error message 38 | * 39 | * @param errorMsg 40 | * error message to log 41 | */ 42 | 43 | public static void shutDownWithErrMsg(String errorMsg){ 44 | if(errorMsg != null) { 45 | LOG.error(errorMsg); 46 | } 47 | LOG.info("Shutting down ApexUnit"); 48 | Thread.dumpStack(); 49 | System.exit(-1); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/OAuthTokenGeneratorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.client; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.testng.Assert; 17 | import org.testng.annotations.BeforeTest; 18 | import org.testng.annotations.Test; 19 | import org.apache.commons.lang.StringUtils; 20 | import org.junit.Ignore; 21 | 22 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 23 | import com.sforce.cd.apexUnit.client.codeCoverage.OAuthTokenGenerator; 24 | 25 | @Ignore 26 | public class OAuthTokenGeneratorTest { 27 | private final static Logger LOG = LoggerFactory.getLogger(OAuthTokenGeneratorTest.class); 28 | 29 | @BeforeTest 30 | public void setup() { 31 | new CommandLineArgumentsTest().setup(); 32 | } 33 | 34 | /*@Test 35 | public void getOrgToken() { 36 | String orgToken = ""; 37 | // generate/get the orgToken and test if the token has been generated 38 | orgToken = OAuthTokenGenerator.getOrgToken(); 39 | LOG.debug(orgToken + " --> This is the orgToken"); 40 | Assert.assertNotEquals(orgToken, null); 41 | Assert.assertTrue(StringUtils.isNotEmpty(orgToken)); 42 | }*/ 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexMethodCodeCoverageBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class for computing code coverage metrics for each method in a given source class 10 | * @author adarsh.ramakrishna@salesforce.com 11 | */ 12 | 13 | 14 | package com.sforce.cd.apexUnit.report; 15 | 16 | public class ApexMethodCodeCoverageBean { 17 | private int coveredLines = 0; 18 | private int unCoveredLines = 0; 19 | private String apexTestClassID; 20 | private String apexClassorTriggerId; 21 | 22 | public int getCoveredLines() { 23 | return coveredLines; 24 | } 25 | 26 | public void setCoveredLines(int coveredLines) { 27 | this.coveredLines = coveredLines; 28 | } 29 | 30 | public int getUnCoveredLines() { 31 | return unCoveredLines; 32 | } 33 | 34 | public void setUnCoveredLines(int unCoveredLines) { 35 | this.unCoveredLines = unCoveredLines; 36 | } 37 | 38 | public String getApexTestClassID() { 39 | return apexTestClassID; 40 | } 41 | 42 | public void setApexTestClassID(String apexTestClassID) { 43 | this.apexTestClassID = apexTestClassID; 44 | } 45 | 46 | public String getApexClassorTriggerId() { 47 | return apexClassorTriggerId; 48 | } 49 | 50 | public void setApexClassorTriggerId(String apexClassorTriggerId) { 51 | this.apexClassorTriggerId = apexClassorTriggerId; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, salesforce.com, inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided 5 | that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the 8 | following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 11 | the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or 14 | promote products derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, salesforce.com, inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided 5 | # that the following conditions are met: 6 | # 7 | # Redistributions of source code must retain the above copyright notice, this list of conditions and the 8 | # following disclaimer. 9 | # 10 | # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 11 | # the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | # 13 | # Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | API_VERSION=37.0 26 | MAX_TIME_OUT_IN_MS=1200000 27 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/arguments/PositiveIntegerValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Validator class to validate if the given value is a positive integer , greater than 1. 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | package com.sforce.cd.apexUnit.arguments; 15 | 16 | import com.beust.jcommander.IParameterValidator; 17 | import com.beust.jcommander.ParameterException; 18 | import com.sforce.cd.apexUnit.ApexUnitUtils; 19 | 20 | public class PositiveIntegerValidator implements IParameterValidator { 21 | /* 22 | * Validates if the given value is a positive integer , greater than 1. Used 23 | * to validate the input value for the parameters that expects positive 24 | * integers (non-Javadoc) 25 | * 26 | * @see com.beust.jcommander.IParameterValidator#validate(java.lang.String, 27 | * java.lang.String) 28 | * 29 | * @param name - name of the parameter 30 | * 31 | * @param value - value passed to the parameter 32 | */ 33 | public void validate(String name, String value) throws ParameterException { 34 | try { 35 | if(value == null || value.isEmpty()){ 36 | ApexUnitUtils.shutDownWithDebugLog(new NumberFormatException() , "Null/No value specified for "+ name ); 37 | } 38 | int n = Integer.parseInt(value); 39 | if (n <= 0) { 40 | ApexUnitUtils.shutDownWithErrMsg("ParameterException: Input value for the Parameter " + name 41 | + " should be positive integer (found " + value + ")"); 42 | } 43 | } catch (NumberFormatException e) { 44 | ApexUnitUtils.shutDownWithDebugLog(e,"NumberFormatException:Input value for the Parameter " + name 45 | + " should be positive integer (found " + value + ")"); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/arguments/URLValidatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.arguments; 13 | 14 | import junit.framework.Assert; 15 | 16 | import org.testng.annotations.Test; 17 | 18 | public class URLValidatorTest { 19 | // TODO rewrite test cases since we are halting the program in case of 20 | // ParameterException 21 | // @Test 22 | public void validateUrlNegativeTest() { 23 | URLValidator urlValidator = new URLValidator(); 24 | String name = "testInvalidName"; 25 | String value = "www.Invalid url"; 26 | String result = urlValidator.validateUrl(name, value); 27 | Assert.assertEquals("Parameter " + name + " should be a valid URL (found " + value + ")", result); 28 | } 29 | 30 | // @Test 31 | public void validateUrlEndingWithDotTest() { 32 | URLValidator urlValidator = new URLValidator(); 33 | String name = "testDotEndingInvalidName"; 34 | String value = "https://www.salesforce."; 35 | String result = urlValidator.validateUrl(name, value); 36 | Assert.assertEquals("Parameter " + name + " should be a valid URL (found " + value + ")", result); 37 | } 38 | 39 | @Test 40 | public void validateUrlPositiveTest() { 41 | URLValidator urlValidator = new URLValidator(); 42 | String name = "testName_HTTPS"; 43 | String value = "https://www.salesforce.com"; 44 | String result = urlValidator.validateUrl(name, value); 45 | Assert.assertEquals("validUrl", result); 46 | } 47 | 48 | @Test 49 | public void validateHTTPUrlPositiveTest() { 50 | URLValidator urlValidator = new URLValidator(); 51 | String name = "testName_HTTP"; 52 | String value = "http://login.salesforce.com"; 53 | String result = urlValidator.validateUrl(name, value); 54 | Assert.assertEquals("validUrl", result); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, salesforce.com, inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided 5 | # that the following conditions are met: 6 | # 7 | # Redistributions of source code must retain the above copyright notice, this list of conditions and the 8 | # following disclaimer. 9 | # 10 | # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 11 | # the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | # 13 | # Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or 14 | # promote products derived from this software without specific prior written permission. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 17 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | # POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | 26 | 27 | 28 | 29 | %d{HH:mm:ss} %-5level %logger{36} - %msg%n 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/ConnectionHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.client; 13 | 14 | import junit.framework.Assert; 15 | 16 | import org.junit.Ignore; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.testng.annotations.Test; 20 | 21 | import com.sforce.async.BulkConnection; 22 | import com.sforce.cd.apexUnit.ApexUnitUtils; 23 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 24 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 25 | import com.sforce.soap.partner.LoginResult; 26 | import com.sforce.soap.partner.PartnerConnection; 27 | import com.sforce.ws.ConnectionException; 28 | import com.sforce.ws.ConnectorConfig; 29 | 30 | @Ignore 31 | public class ConnectionHandlerTest { 32 | 33 | ConnectionHandler connHandler = ConnectionHandler.getConnectionHandlerInstance(); 34 | private static Logger LOG = LoggerFactory.getLogger(ConnectionHandlerTest.class); 35 | 36 | /*@Test(priority = 1) 37 | public void createConnection() throws ConnectionException { 38 | PartnerConnection partnerConn = connHandler.getConnection(); 39 | LoginResult loginRes = null; 40 | if (partnerConn != null) { 41 | loginRes = partnerConn.login(CommandLineArguments.getUsername(), CommandLineArguments.getPassword()); 42 | } 43 | if (loginRes != null) { 44 | Assert.assertEquals(false, loginRes.getPasswordExpired()); 45 | } else { 46 | ApexUnitUtils.shutDownWithErrMsg("Unable to create Connection.. Failed test!"); 47 | } 48 | } 49 | 50 | @Test(priority = 2) 51 | public void getBulkConnection() { 52 | String sessionId = connHandler.getSessionIdFromConnectorConfig(); 53 | BulkConnection bulkConn = connHandler.getBulkConnection(); 54 | ConnectorConfig connConfig = bulkConn.getConfig(); 55 | LOG.info("Rest end point:" + connConfig.getRestEndpoint()); 56 | Assert.assertEquals(sessionId, connConfig.getSessionId()); 57 | 58 | }*/ 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/arguments/PercentageInputValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Validator class to validate if a given input is within the percentage range of 0 to 100% 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.arguments; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import com.beust.jcommander.IParameterValidator; 21 | import com.beust.jcommander.ParameterException; 22 | import com.sforce.cd.apexUnit.ApexUnitUtils; 23 | 24 | public class PercentageInputValidator implements IParameterValidator { 25 | private static Logger LOG = LoggerFactory.getLogger(PercentageInputValidator.class); 26 | 27 | /* 28 | * Validates if the given value lies in the range of 0 to 100. Used to 29 | * validate the input value for parameters that expects percentage 30 | * (non-Javadoc) 31 | * 32 | * @see com.beust.jcommander.IParameterValidator#validate(java.lang.String, 33 | * java.lang.String) 34 | * 35 | * @param name - name of the parameter 36 | * 37 | * @param value - value passed to the parameter 38 | */ 39 | public void validate(String name, String value) throws ParameterException { 40 | try { 41 | int n = Integer.parseInt(value); 42 | if (n < 0) { 43 | ApexUnitUtils.shutDownWithErrMsg("ParameterException: Input value for the Parameter " + name 44 | + " should be positive integer (found " + value + ")"); 45 | } 46 | if (n > 100) { 47 | ApexUnitUtils.shutDownWithErrMsg("ParameterException:Input value for the Parameter " + name 48 | + " should be in the range of 0 to 100 (found " + value + ")"); 49 | } 50 | } catch (NumberFormatException e) { 51 | LOG.debug("Value provided for the parameter " + name + " is out of range. " 52 | + "The value must be in the range of 0-100"); 53 | ApexUnitUtils.shutDownWithDebugLog(e,"NumberFormatException:Input value for the Parameter " + name 54 | + " should be positive integer (found " + value + ")"); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/PartnerConnectionConnectorConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | package com.sforce.cd.apexUnit.client; 8 | 9 | import junit.framework.Assert; 10 | 11 | import org.junit.Ignore; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.testng.annotations.BeforeTest; 15 | import org.testng.annotations.Test; 16 | 17 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 18 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 19 | import com.sforce.cd.apexUnit.client.connection.PartnerConnectionConnectorConfig; 20 | import com.sforce.soap.partner.PartnerConnection; 21 | import com.sforce.ws.ConnectionException; 22 | import com.sforce.ws.ConnectorConfig; 23 | 24 | @Ignore 25 | public class PartnerConnectionConnectorConfigTest { 26 | private static Logger LOG = LoggerFactory.getLogger(PartnerConnectionConnectorConfigTest.class); 27 | 28 | @BeforeTest 29 | public void setup() { 30 | new CommandLineArgumentsTest().setup(); 31 | } 32 | 33 | /*@Test 34 | public void testConfigForSessionRenewParam() throws ConnectionException { 35 | PartnerConnectionConnectorConfig pcConnectorConfig = new PartnerConnectionConnectorConfig(); 36 | // Instantiate Connection Handler so that it sets max time out value 37 | ConnectionHandler connHandler = ConnectionHandler.getConnectionHandlerInstance(); 38 | PartnerConnection connection = connHandler.getConnection(); 39 | ConnectorConfig config = pcConnectorConfig.createConfig(); 40 | LOG.debug("testConfigForSessionRenewParam() test .. " 41 | + config.getSessionRenewer().renewSession(config).headerElement); 42 | // assert and check if timeout has been set accurately 43 | Assert.assertEquals(config.getConnectionTimeout(), ConnectionHandler.MAX_TIME_OUT_IN_MS_INT); 44 | // assert and check if the session info for connector config info for 45 | // session renewer component matches connection's session info 46 | Assert.assertEquals(config.getSessionRenewer().renewSession(config).headerElement, 47 | connection.getSessionHeader()); 48 | }*/ 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/codeCoverage/OAuthTokenGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to generate OAuth token for the given org and login credentials 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | package com.sforce.cd.apexUnit.client.codeCoverage; 14 | 15 | import java.util.HashMap; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import com.sforce.cd.apexUnit.ApexUnitUtils; 21 | 22 | /* 23 | * Generates OAuth Token 24 | * Usage: the token can be used to invoke web services 25 | * and leverage features provided by force.com platform like Tooling APIs 26 | */ 27 | public class OAuthTokenGenerator { 28 | private static Logger LOG = LoggerFactory.getLogger(OAuthTokenGenerator.class); 29 | private static String orgToken = ""; 30 | 31 | /* 32 | * returns oauth org token 33 | */ 34 | public static String getOrgToken() { 35 | if (orgToken.equals("")) { 36 | orgToken = doPostAndGetOrgToken(); 37 | } 38 | return orgToken; 39 | } 40 | 41 | /* 42 | * sends out post request and fetches the org token for the org 43 | */ 44 | private static String doPostAndGetOrgToken() { 45 | String oAuthTokenServiceUrl = "/services/oauth2/token"; 46 | String orgToken = null; 47 | WebServiceInvoker webServiceInvoker = new WebServiceInvoker(); 48 | // get response map using a post web service call to the service url 49 | HashMap responseMap = webServiceInvoker.doPost(oAuthTokenServiceUrl); 50 | if (responseMap != null && responseMap.containsKey("access_token")) { 51 | orgToken = responseMap.get("access_token"); 52 | LOG.debug("Org token : " + orgToken); 53 | } else { 54 | ApexUnitUtils.shutDownWithErrMsg( 55 | "Unable to get access_token for OAuth authentication and hence unable to establish connection with the web services." 56 | + "Terminating the process.."); 57 | } 58 | // TODO setting session as cookie -- need to look into this later so 59 | // that the session can be handled more efficiently 60 | // HttpServletResponse httpResponse = (HttpServletResponse)responseBody; 61 | // Cookie session = new Cookie(ACCESS_TOKEN, result); 62 | // session.setMaxAge(-1); //cookie not persistent, destroyed on browser 63 | // exit 64 | // httpResponse.addCookie(session); 65 | 66 | return orgToken; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexReportBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * This class represents the Bean object for test execution results for a given Apex test class 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.report; 16 | 17 | /* 18 | * ApexReportBean stores the results of execution of Apex test classes 19 | */ 20 | public class ApexReportBean { 21 | 22 | private String Outcome; 23 | private String ApexClassName; 24 | private String ApexClassId; 25 | private String MethodName; 26 | private String Message; 27 | private String StackTrace; 28 | private Integer passedTestsCount; 29 | private Integer failedTestsCount; 30 | private long timeElapsed; 31 | 32 | public String getOutcome() { 33 | return Outcome; 34 | } 35 | 36 | public void setOutcome(String outcome) { 37 | Outcome = outcome; 38 | } 39 | 40 | public String getApexClassName() { 41 | return ApexClassName; 42 | } 43 | 44 | public void setApexClassName(String apexClassName) { 45 | ApexClassName = apexClassName; 46 | } 47 | 48 | public String getApexClassId() { 49 | return ApexClassId; 50 | } 51 | 52 | public void setApexClassId(String apexClassId) { 53 | ApexClassId = apexClassId; 54 | } 55 | 56 | public String getMethodName() { 57 | return MethodName; 58 | } 59 | 60 | public void setMethodName(String methodName) { 61 | MethodName = methodName; 62 | } 63 | 64 | public String getMessage() { 65 | return Message; 66 | } 67 | 68 | public void setMessage(String message) { 69 | Message = message; 70 | } 71 | 72 | public String getStackTrace() { 73 | return StackTrace; 74 | } 75 | 76 | public void setStackTrace(String stackTrace) { 77 | StackTrace = stackTrace; 78 | } 79 | 80 | public Integer getPassedTestsCount() { 81 | return passedTestsCount; 82 | } 83 | 84 | public void setPassedTestsCount(Integer passedTestsCount) { 85 | this.passedTestsCount = passedTestsCount; 86 | } 87 | 88 | public Integer getFailedTestsCount() { 89 | return failedTestsCount; 90 | } 91 | 92 | public void setFailedTestsCount(Integer failedTestsCount) { 93 | this.failedTestsCount = failedTestsCount; 94 | } 95 | 96 | public long getTimeElapsed() { 97 | return timeElapsed; 98 | } 99 | 100 | public void setTimeElapsed(long timeElapsed) { 101 | this.timeElapsed = timeElapsed; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/arguments/URLValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * validator class to validate if the input to the url field matches the url regex pattern. 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.arguments; 16 | 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import com.beust.jcommander.IParameterValidator; 24 | import com.beust.jcommander.ParameterException; 25 | import com.sforce.cd.apexUnit.ApexUnitUtils; 26 | 27 | public class URLValidator implements IParameterValidator { 28 | private static Logger LOG = LoggerFactory.getLogger(URLValidator.class); 29 | 30 | /* 31 | * overridden method for implementing IParameterValidator (non-Javadoc) 32 | * 33 | * @see com.beust.jcommander.IParameterValidator#validate(java.lang.String, 34 | * java.lang.String) validates URL pattern 35 | * 36 | * @param name - name of the parameter 37 | * 38 | * @param value - value passed to the parameter 39 | */ 40 | public void validate(String name, String value) throws ParameterException { 41 | boolean validUrl = false; 42 | // regex to be matched for a url to be considered valid 43 | // supports both http and https regex patterns 44 | String regex = "\\b(https?)://[-a-zA-Z0-9+&@#\\/%?=~_|!:,.;]+.[-a-zA-Z]+"; 45 | Pattern urlPattern = Pattern.compile(regex); 46 | Matcher matcher = urlPattern.matcher(value); 47 | validUrl = matcher.matches(); 48 | if (validUrl == false) { 49 | LOG.debug("Invalid url provided for the parameter : " + name + ". Please follow the below regex format: " 50 | + regex); 51 | ApexUnitUtils.shutDownWithErrMsg( 52 | "ParameterException:Parameter " + name + " should be a valid URL (found " + value + ")"); 53 | } 54 | } 55 | 56 | /* 57 | * Takes in the name and value of the url string and returns a String which 58 | * can be used by Assert statements of corresponding test method 59 | * 60 | * TODO: Have better mechanism in validateURL than using property file value 61 | * for test assertion 62 | * 63 | * @param name - name of the parameter 64 | * 65 | * @param value - value passed to the parameter 66 | */ 67 | public String validateUrl(String name, String value) { 68 | try { 69 | validate(name, value); 70 | } catch (ParameterException paramEx) { 71 | ApexUnitUtils.shutDownWithDebugLog(paramEx, paramEx.getMessage()); 72 | } 73 | return "validUrl"; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/ToolingAPIInvokerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.client; 13 | 14 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 15 | import com.sforce.cd.apexUnit.client.codeCoverage.WebServiceInvoker; 16 | import org.junit.Assert; 17 | import org.junit.Ignore; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.testng.annotations.BeforeTest; 21 | import org.testng.annotations.Test; 22 | 23 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 24 | import com.sforce.cd.apexUnit.client.codeCoverage.CodeCoverageComputer; 25 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 26 | import com.sforce.cd.apexUnit.report.ApexClassCodeCoverageBean; 27 | import com.sforce.soap.partner.PartnerConnection; 28 | 29 | import java.io.UnsupportedEncodingException; 30 | 31 | import static java.net.URLEncoder.encode; 32 | 33 | @Ignore 34 | public class ToolingAPIInvokerTest { 35 | CodeCoverageComputer codeCoverageComputer = null; 36 | PartnerConnection conn = null; 37 | private static final Logger LOG = LoggerFactory.getLogger(ToolingAPIInvokerTest.class); 38 | 39 | @BeforeTest 40 | public void setup() { 41 | new CommandLineArgumentsTest().setup(); 42 | codeCoverageComputer = new CodeCoverageComputer(); 43 | ConnectionHandler connectionHandler = ConnectionHandler.getConnectionHandlerInstance(); 44 | //conn = connectionHandler.getConnection(); 45 | } 46 | 47 | /*@Test 48 | public void calculateAggregatedCodeCoverageUsingToolingAPITest() { 49 | // TODO Assert on contents of apexClassCodeCoverageBeans over to size 50 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = codeCoverageComputer 51 | .calculateAggregatedCodeCoverageUsingToolingAPI(); 52 | Assert.assertTrue(apexClassCodeCoverageBeans != null && apexClassCodeCoverageBeans.length > 0); 53 | } 54 | 55 | @Test 56 | public void getOrgWideCodeCoverageTest() { 57 | int orgWideCodeCoverage = -1; 58 | orgWideCodeCoverage = codeCoverageComputer.getOrgWideCodeCoverage(); 59 | LOG.info("orgWideCodeCoverage: " + orgWideCodeCoverage); 60 | Assert.assertTrue(orgWideCodeCoverage >= 0); 61 | } 62 | 63 | @Test 64 | public void RequestStringPasswordIsEncodedTest() throws UnsupportedEncodingException { 65 | WebServiceInvoker wsi = new WebServiceInvoker(); 66 | String requestString = wsi.generateRequestString(); 67 | String passwordIdentifier = "&password="; 68 | String encodedPassword = encode(CommandLineArguments.getPassword(), "UTF-8"); 69 | int indexOfpasswordIdentifier = requestString.indexOf(passwordIdentifier); 70 | String passwordInRequestString = requestString.substring(indexOfpasswordIdentifier + passwordIdentifier.length()); 71 | org.testng.Assert.assertEquals(encodedPassword, passwordInRequestString); 72 | }*/ 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/arguments/PercentageInputValidatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.arguments; 13 | 14 | import junit.framework.Assert; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.testng.annotations.Test; 19 | 20 | import com.beust.jcommander.ParameterException; 21 | 22 | public class PercentageInputValidatorTest { 23 | 24 | private static Logger LOG = LoggerFactory.getLogger(PercentageInputValidatorTest.class); 25 | 26 | // TODO rewrite test cases since we are halting the program in case of 27 | // ParameterException 28 | // @Test 29 | public void validateStringNegativeTest() { 30 | PercentageInputValidator percentInputValidator = new PercentageInputValidator(); 31 | String name = "codeCoverageThreshold"; 32 | String value = "abc"; 33 | String referenceResult = "valid argument"; 34 | String result = referenceResult; 35 | try { 36 | percentInputValidator.validate(name, value); 37 | } catch (ParameterException e) { 38 | result = e.getMessage(); 39 | } 40 | LOG.debug("result:" + result); 41 | Assert.assertTrue(!result.equals(referenceResult)); 42 | } 43 | 44 | // @Test 45 | public void validateIntUnderLimitNegativeTest() { 46 | PercentageInputValidator percentInputValidator = new PercentageInputValidator(); 47 | String name = "codeCoverageThreshold"; 48 | String value = "0.01"; 49 | String referenceResult = "valid argument"; 50 | String result = referenceResult; 51 | try { 52 | percentInputValidator.validate(name, value); 53 | } catch (ParameterException e) { 54 | result = e.getMessage(); 55 | } 56 | LOG.debug("result:" + result); 57 | Assert.assertTrue(!result.equals(referenceResult)); 58 | } 59 | 60 | // @Test 61 | public void validateIntOverLimitNegativeTest() { 62 | PercentageInputValidator percentInputValidator = new PercentageInputValidator(); 63 | String name = "codeCoverageThreshold"; 64 | String value = "1001"; 65 | String referenceResult = "valid argument"; 66 | String result = referenceResult; 67 | try { 68 | percentInputValidator.validate(name, value); 69 | } catch (ParameterException e) { 70 | result = e.getMessage(); 71 | } 72 | LOG.debug("result:" + result); 73 | Assert.assertTrue(!result.equals(referenceResult)); 74 | } 75 | 76 | @Test 77 | public void validatePositiveTest() { 78 | PercentageInputValidator percentInputValidator = new PercentageInputValidator(); 79 | String name = "codeCoverageThreshold"; 80 | String value = "80"; 81 | String referenceResult = "valid argument"; 82 | String result = referenceResult; 83 | try { 84 | percentInputValidator.validate(name, value); 85 | } catch (ParameterException e) { 86 | result = e.getMessage(); 87 | } 88 | LOG.debug("result:" + result); 89 | Assert.assertTrue(result.equals(referenceResult)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexClassCodeCoverageBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * This class represents the Bean object for code coverage computation for source classes 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.report; 16 | 17 | import java.util.List; 18 | 19 | public class ApexClassCodeCoverageBean implements Comparable { 20 | 21 | private String apexTestClassID; 22 | private String apexClassorTriggerId; 23 | private String apexClassName; 24 | private int numLinesCovered = 0; 25 | private int numLinesUncovered = 0; 26 | private ApexMethodCodeCoverageBean[] testMethodNames; 27 | private String apiVersion; 28 | private String lengthWithoutComments; 29 | private List coveredLinesList; 30 | private List uncoveredLinesList; 31 | 32 | public List getCoveredLinesList() { 33 | return coveredLinesList; 34 | } 35 | 36 | public void setCoveredLinesList(List coveredLinesList) { 37 | this.coveredLinesList = coveredLinesList; 38 | } 39 | 40 | public List getUncoveredLinesList() { 41 | return uncoveredLinesList; 42 | } 43 | 44 | public void setUncoveredLinesList(List uncoveredLinesList) { 45 | this.uncoveredLinesList = uncoveredLinesList; 46 | } 47 | 48 | public String getApexTestClassID() { 49 | return apexTestClassID; 50 | } 51 | 52 | public void setApexTestClassID(String apexTestClassID) { 53 | this.apexTestClassID = apexTestClassID; 54 | } 55 | 56 | public String getApexClassorTriggerId() { 57 | return apexClassorTriggerId; 58 | } 59 | 60 | public void setApexClassorTriggerId(String apexClassorTriggerId) { 61 | this.apexClassorTriggerId = apexClassorTriggerId; 62 | } 63 | 64 | public String getApexClassName() { 65 | return apexClassName; 66 | } 67 | 68 | public void setApexClassName(String apexClassName) { 69 | this.apexClassName = apexClassName; 70 | } 71 | 72 | public int getNumLinesCovered() { 73 | return numLinesCovered; 74 | } 75 | 76 | public void setNumLinesCovered(int numLinesCovered) { 77 | this.numLinesCovered = numLinesCovered; 78 | } 79 | 80 | public int getNumLinesUncovered() { 81 | return numLinesUncovered; 82 | } 83 | 84 | public void setNumLinesUncovered(int numLinesUncovered) { 85 | this.numLinesUncovered = numLinesUncovered; 86 | } 87 | 88 | public ApexMethodCodeCoverageBean[] getTestMethodNames() { 89 | return testMethodNames; 90 | } 91 | 92 | public void setTestMethodNames(ApexMethodCodeCoverageBean[] testMethodNames) { 93 | this.testMethodNames = testMethodNames; 94 | } 95 | 96 | public String getApiVersion() { 97 | return apiVersion; 98 | } 99 | 100 | public void setApiVersion(String apiVersion) { 101 | this.apiVersion = apiVersion; 102 | } 103 | 104 | public String getLengthWithoutComments() { 105 | return lengthWithoutComments; 106 | } 107 | 108 | public void setLengthWithoutComments(String lengthWithoutComments) { 109 | this.lengthWithoutComments = lengthWithoutComments; 110 | } 111 | 112 | public double getCoveragePercentage() { 113 | double totalLines = numLinesCovered + numLinesUncovered; 114 | if (totalLines > 0) { 115 | return (numLinesCovered / (totalLines)) * 100.0; 116 | } else { 117 | return 100.0; 118 | } 119 | } 120 | 121 | /* 122 | * over riding compareTo method of Comparable interface 123 | * 124 | * (non-Javadoc) 125 | * 126 | * @see java.lang.Comparable#compareTo(java.lang.Object) 127 | */ 128 | public int compareTo(ApexClassCodeCoverageBean codeCoverageBean) { 129 | if (this.getCoveragePercentage() < codeCoverageBean.getCoveragePercentage()) { 130 | return -1; 131 | } else if (this.getCoveragePercentage() > codeCoverageBean.getCoveragePercentage()) { 132 | return 1; 133 | } else { 134 | return 0; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexUnitTestReportGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to generate test report for ApexUnit run in JUnit test report format 10 | * @author adarsh.ramakrishna@salesforce.com 11 | */ 12 | 13 | 14 | package com.sforce.cd.apexUnit.report; 15 | 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.util.HashMap; 19 | 20 | import org.jdom.Document; 21 | import org.jdom.Element; 22 | import org.jdom.output.XMLOutputter; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import com.sforce.cd.apexUnit.ApexUnitUtils; 27 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 28 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 29 | import com.sforce.soap.partner.PartnerConnection; 30 | 31 | public class ApexUnitTestReportGenerator { 32 | private static Logger LOG = LoggerFactory.getLogger(ApexUnitTestReportGenerator.class); 33 | 34 | /** 35 | * Generates a JUnit/Jenkins compliant test report in XML for the given job. 36 | * 37 | * @param reportBeans 38 | * @param reportFile 39 | * of the job whose test report is to be generated 40 | * 41 | */ 42 | public static void generateTestReport(ApexReportBean[] reportBeans, String reportFile) { 43 | if (reportBeans != null && reportBeans.length > 0) { 44 | // Create root element 45 | Document document = new Document(); 46 | Element rootElement = new Element("testsuite"); 47 | document.setRootElement(rootElement); 48 | 49 | int failureCount = 0; 50 | int testCount = 0; 51 | Long totalTimeInMillis = 0L; 52 | 53 | for (ApexReportBean reportBean : reportBeans) { 54 | testCount++; 55 | 56 | // Create testcase element retrieving values from the result 57 | Element testcase = new Element("testcase"); 58 | String apexClassName = ""; 59 | // set apex ClassName 60 | if (reportBean.getApexClassName() == null || reportBean.getApexClassName().equals("")) { 61 | 62 | if (reportBean.getApexClassId() != null 63 | && ApexClassFetcherUtils.apexClassMap.get(reportBean.getApexClassId()) != null) { 64 | apexClassName = ApexClassFetcherUtils.apexClassMap.get(reportBean.getApexClassId()); 65 | } else if (reportBean.getApexClassId() != null) { 66 | PartnerConnection conn = ConnectionHandler.getConnectionHandlerInstance().getConnection(); 67 | HashMap apexClassInfoMap = ApexClassFetcherUtils.fetchApexClassInfoFromId(conn, 68 | reportBean.getApexClassId()); 69 | apexClassName = apexClassInfoMap.get("Name"); 70 | } else { 71 | LOG.debug( 72 | "Report bean not constructed properly. 'null' class ID associated with the report bean"); 73 | } 74 | // By now apexClassName will be populated 75 | reportBean.setApexClassName(apexClassName); 76 | } else { 77 | apexClassName = reportBean.getApexClassName(); 78 | } 79 | testcase.setAttribute("classname", apexClassName); 80 | // set class test method 81 | testcase.setAttribute("name", reportBean.getMethodName()); 82 | 83 | // set time and accumulate for class 84 | Long timeInMillis = new Long(reportBean.getTimeElapsed()); 85 | totalTimeInMillis += timeInMillis; 86 | testcase.setAttribute("time", Double.toString(timeInMillis.doubleValue() / 1000)); 87 | 88 | // Increment pass/fail counters 89 | if (reportBean.getOutcome().equalsIgnoreCase("pass")) { 90 | Element success = new Element("Success"); 91 | success.setAttribute("message", "Passed"); 92 | testcase.addContent(success); 93 | } else if (reportBean.getOutcome().equalsIgnoreCase("fail") 94 | | reportBean.getOutcome().equalsIgnoreCase("compilefail")) { 95 | failureCount++; 96 | Element failure = new Element("failure"); 97 | failure.setAttribute("message", reportBean.getMessage()); 98 | failure.setText(reportBean.getStackTrace()); 99 | testcase.addContent(failure); 100 | } 101 | 102 | rootElement.addContent(testcase); 103 | } 104 | 105 | // Add the pass, fail counters as attributes to root element 106 | rootElement.setAttribute("failures", Integer.toString(failureCount)); 107 | rootElement.setAttribute("tests", Integer.toString(testCount)); 108 | rootElement.setAttribute("time", Double.toString(totalTimeInMillis.doubleValue() / 1000)); 109 | 110 | // Write the DOM to xml file 111 | try { 112 | XMLOutputter outputter = new XMLOutputter(); 113 | outputter.output(document, new FileOutputStream(reportFile)); 114 | } catch (IOException e) { 115 | ApexUnitUtils 116 | .shutDownWithDebugLog(e, "IOException encountered while trying to write the ApexUnit test report to the file " 117 | + reportFile); 118 | } 119 | } else { 120 | ApexUnitUtils.shutDownWithErrMsg( 121 | "Unable to generate test report. " + "Did not find any test results for the job id"); 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/ApexClassFetcherUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | package com.sforce.cd.apexUnit.client; 12 | 13 | import java.util.HashMap; 14 | 15 | import junit.framework.Assert; 16 | 17 | import org.junit.Ignore; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.testng.annotations.BeforeTest; 21 | import org.testng.annotations.Test; 22 | 23 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 24 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 25 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 26 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 27 | import com.sforce.soap.partner.PartnerConnection; 28 | 29 | @Ignore 30 | public class ApexClassFetcherUtilsTest { 31 | private static Logger LOG = LoggerFactory.getLogger(ApexClassFetcherUtilsTest.class); 32 | ConnectionHandler connHandler = null; 33 | PartnerConnection conn = null; 34 | String soql = null; 35 | HashMap localMap = new HashMap(); 36 | 37 | @BeforeTest 38 | public void setup() { 39 | new CommandLineArgumentsTest().setup(); 40 | connHandler = ConnectionHandler.getConnectionHandlerInstance(); 41 | //conn = connHandler.getConnection(); 42 | } 43 | 44 | /*@Test 45 | public void constructTestClassesArrayTest() { 46 | String[] testClasses = ApexClassFetcherUtils.constructTestClassesArray(conn); 47 | if (testClasses != null) { 48 | Assert.assertTrue(testClasses.length > 0 || ApexClassFetcherUtils.apexClassMap.size() > 0); 49 | } 50 | } 51 | 52 | @Test 53 | public void fetchApexClassesFromManifestFilesTest() { 54 | String[] testClasses = ApexClassFetcherUtils 55 | .fetchApexClassesFromManifestFiles(CommandLineArguments.getTestManifestFiles(), true); 56 | if (testClasses != null && ApexClassFetcherUtils.apexClassMap != null 57 | && ApexClassFetcherUtils.apexClassMap.size() != testClasses.length) { 58 | Assert.assertTrue(testClasses.length > 0 || ApexClassFetcherUtils.apexClassMap.size() > 0); 59 | } 60 | } 61 | 62 | @Test 63 | public void fetchApexClassesBasedOnPrefixTest() { 64 | String[] testClasses = ApexClassFetcherUtils.fetchApexClassesBasedOnMultipleRegexes(conn, null, 65 | CommandLineArguments.getTestRegex(), true); 66 | if (testClasses != null) { 67 | Assert.assertTrue(testClasses.length > 0 || ApexClassFetcherUtils.apexClassMap.size() > 0); 68 | } 69 | } 70 | 71 | @Test(priority = 1) 72 | public void constructTestClassArrayUsingWSC() { 73 | soql = QueryConstructor.generateQueryToFetchApexClassesBasedOnRegex(null, CommandLineArguments.getTestRegex()); 74 | String[] testClasses = ApexClassFetcherUtils.constructClassIdArrayUsingWSC(conn, soql); 75 | logFilteredTestClasses(testClasses); 76 | if (testClasses != null) { 77 | Assert.assertTrue(testClasses.length > 0 || ApexClassFetcherUtils.apexClassMap.size() > 0); 78 | } 79 | } 80 | 81 | @Test(priority = 2) 82 | public void fetchApexClassIdFromNameTest() { 83 | String className = null; 84 | String expectedTestClassId = null; 85 | // pass empty string so that random classes gets picked 86 | soql = QueryConstructor.generateQueryToFetchApexClassesBasedOnRegex(null, "*"); 87 | // limit the result to 1 . Thats all we need to test the method 88 | // fetchApexClassIdFromName 89 | soql += " limit 1"; 90 | // the below call populates ApexClassFetcher.apexClassMap 91 | String[] testClasses = ApexClassFetcherUtils.constructClassIdArrayUsingWSC(conn, soql); 92 | if (testClasses != null && ApexClassFetcherUtils.apexClassMap != null) { 93 | for (String testClass : testClasses) { 94 | className = ApexClassFetcherUtils.apexClassMap.get(testClass); 95 | expectedTestClassId = testClass; 96 | } 97 | } 98 | soql = QueryConstructor.generateQueryToFetchApexClass(null, className); 99 | String testClassId = ApexClassFetcherUtils.fetchAndAddToMapApexClassIdBasedOnName(conn, soql); 100 | Assert.assertEquals(expectedTestClassId, testClassId); 101 | } 102 | 103 | @Test(priority = 2) 104 | public void fetchApexClassNameFromIdTest() { 105 | String classId = null; 106 | String expectedClassName = null; 107 | // pass empty string so that random classes gets picked 108 | soql = QueryConstructor.generateQueryToFetchApexClassesBasedOnRegex(null,"*"); 109 | // limit the result to 1 . Thats all we need to test the method 110 | // fetchApexClassIdFromName 111 | soql += " limit 1"; 112 | // the below call populates ApexClassFetcher.apexClassMap 113 | String[] testClasses = ApexClassFetcherUtils.constructClassIdArrayUsingWSC(conn, soql); 114 | if (testClasses != null && ApexClassFetcherUtils.apexClassMap != null) { 115 | for (String testClassId : testClasses) { 116 | expectedClassName = ApexClassFetcherUtils.apexClassMap.get(testClassId); 117 | classId = testClassId; 118 | } 119 | } 120 | if (classId != null) { 121 | HashMap apexClassInfoMap = ApexClassFetcherUtils.fetchApexClassInfoFromId(conn, classId); 122 | String className = apexClassInfoMap.get("Name"); 123 | Assert.assertEquals(expectedClassName, className); 124 | } 125 | } 126 | 127 | private void logFilteredTestClasses(String[] testClasses) { 128 | LOG.debug("Fetched apex classes from the test: "); 129 | for (int i = 0; i < testClasses.length; i++) { 130 | LOG.debug(testClasses[i]); 131 | } 132 | }*/ 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/report/ApexReportGeneratorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.report; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | 17 | import junit.framework.Assert; 18 | 19 | import org.apache.commons.io.FileUtils; 20 | import org.junit.Ignore; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.testng.annotations.AfterTest; 24 | import org.testng.annotations.BeforeTest; 25 | import org.testng.annotations.Test; 26 | 27 | import com.sforce.cd.apexUnit.ApexUnitUtils; 28 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 29 | import com.sforce.cd.apexUnit.client.codeCoverage.CodeCoverageComputer; 30 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 31 | import com.sforce.cd.apexUnit.client.testEngine.TestStatusPollerAndResultHandler; 32 | import com.sforce.soap.partner.PartnerConnection; 33 | import com.sforce.soap.partner.QueryResult; 34 | import com.sforce.soap.partner.sobject.SObject; 35 | import com.sforce.ws.ConnectionException; 36 | 37 | @Ignore 38 | public class ApexReportGeneratorTest { 39 | 40 | private static final Logger LOG = LoggerFactory.getLogger(ApexReportGeneratorTest.class); 41 | PartnerConnection conn = null; 42 | String parentJobId = null; 43 | 44 | @BeforeTest 45 | public void setup() { 46 | /*new CommandLineArgumentsTest().setup(); 47 | cleanUpReports(); 48 | ConnectionHandler connectionHandler = ConnectionHandler.getConnectionHandlerInstance(); 49 | conn = connectionHandler.getConnection(); 50 | // pick the most recent parentJobId 51 | String soql = "SELECT AsyncApexJobId,SystemModstamp,TestTimestamp FROM ApexTestResult ORDER BY SystemModstamp DESC LIMIT 1"; 52 | QueryResult queryResult; 53 | try { 54 | queryResult = conn.query(soql); 55 | 56 | if (queryResult.getDone()) { 57 | SObject[] sObjects = queryResult.getRecords(); 58 | if (sObjects != null) { 59 | for (SObject sobject : sObjects) { 60 | Object parentJob = sobject.getField("AsyncApexJobId"); 61 | if(parentJob !=null){ 62 | parentJobId = sobject.getField("AsyncApexJobId").toString(); 63 | } 64 | } 65 | } 66 | } 67 | } catch (ConnectionException e) { 68 | ApexUnitUtils.shutDownWithDebugLog(e, "ConnectionException : "); 69 | }*/ 70 | } 71 | 72 | /*@Test 73 | public void generateTestReportTest() { 74 | 75 | TestStatusPollerAndResultHandler queryPollerAndResultHandler = new TestStatusPollerAndResultHandler(); 76 | Long justBeforeReportGeneration = System.currentTimeMillis(); 77 | ApexReportBean[] apexReportBeans = queryPollerAndResultHandler.fetchResultsFromParentJobId(parentJobId, conn); 78 | CodeCoverageComputer toolingAPIInvoker = new CodeCoverageComputer(); 79 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = toolingAPIInvoker 80 | .calculateAggregatedCodeCoverageUsingToolingAPI(); 81 | String reportFileName = "ApexUnitReport.xml"; 82 | 83 | ApexUnitTestReportGenerator.generateTestReport(apexReportBeans, reportFileName); 84 | 85 | File reportFile = new File(reportFileName); 86 | LOG.info("justBeforeReportGeneration: " + justBeforeReportGeneration); 87 | LOG.info("reportFile last modified..: " + reportFile.lastModified()); 88 | LOG.info("Result:" + FileUtils.isFileNewer(reportFile, justBeforeReportGeneration)); 89 | Assert.assertTrue(FileUtils.isFileNewer(reportFile, justBeforeReportGeneration)); 90 | } 91 | 92 | @Test 93 | public void generateHTMLReportTest() { 94 | TestStatusPollerAndResultHandler queryPollerAndResultHandler = new TestStatusPollerAndResultHandler(); 95 | ApexReportBean[] apexReportBeans = queryPollerAndResultHandler.fetchResultsFromParentJobId(parentJobId, conn); 96 | CodeCoverageComputer codeCoverageComputer = new CodeCoverageComputer(); 97 | Long justBeforeReportGeneration = System.currentTimeMillis(); 98 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = codeCoverageComputer 99 | .calculateAggregatedCodeCoverageUsingToolingAPI(); 100 | 101 | if (apexClassCodeCoverageBeans != null) { 102 | ApexCodeCoverageReportGenerator.generateHTMLReport(apexClassCodeCoverageBeans); 103 | } 104 | String reportFilePath = System.getProperty("user.dir") + System.getProperty("file.separator") + "Report" 105 | + System.getProperty("file.separator") + "ApexUnitReport.html"; 106 | File reportFile = new File(reportFilePath); 107 | 108 | Assert.assertTrue(FileUtils.isFileNewer(reportFile, justBeforeReportGeneration)); 109 | 110 | } 111 | */ 112 | public void cleanUpReports() { 113 | 114 | String testReportPath = System.getProperty("user.dir") + System.getProperty("file.separator") 115 | + "ApexUnitReport.xml"; 116 | File testReport = new File(testReportPath); 117 | if (testReport.exists() && testReport.delete()) { 118 | LOG.info("Test report deleted"); 119 | } else { 120 | LOG.info("Test report not deleted"); 121 | } 122 | String reportDirPath = System.getProperty("user.dir") + System.getProperty("file.separator") + "Report"; 123 | File reportDir = new File(reportDirPath); 124 | if (reportDir.exists()) { 125 | try { 126 | FileUtils.deleteDirectory(reportDir); 127 | } catch (IOException e) { 128 | ApexUnitUtils.shutDownWithDebugLog(e, "IO Exception encountered while deleting the reports"); 129 | } 130 | LOG.info("Report directory deleted"); 131 | } else { 132 | LOG.info("Report directory does not exist; hence not deleted"); 133 | } 134 | 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/client/AsyncBulkApiHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | 12 | package com.sforce.cd.apexUnit.client; 13 | 14 | import java.io.IOException; 15 | import java.util.List; 16 | 17 | import junit.framework.Assert; 18 | 19 | import org.junit.Ignore; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.testng.annotations.BeforeTest; 23 | import org.testng.annotations.Test; 24 | 25 | import com.sforce.async.AsyncApiException; 26 | import com.sforce.async.BatchInfo; 27 | import com.sforce.async.BulkConnection; 28 | import com.sforce.async.JobInfo; 29 | import com.sforce.async.JobStateEnum; 30 | import com.sforce.async.OperationEnum; 31 | import com.sforce.cd.apexUnit.ApexUnitUtils; 32 | import com.sforce.cd.apexUnit.arguments.CommandLineArgumentsTest; 33 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 34 | import com.sforce.cd.apexUnit.client.testEngine.AsyncBulkApiHandler; 35 | import com.sforce.cd.apexUnit.client.testEngine.TestStatusPollerAndResultHandler; 36 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 37 | import com.sforce.cd.apexUnit.report.ApexReportBean; 38 | import com.sforce.soap.partner.PartnerConnection; 39 | import com.sforce.soap.partner.SaveResult; 40 | import com.sforce.ws.ConnectionException; 41 | import com.sforce.ws.ConnectorConfig; 42 | 43 | @Ignore 44 | public class AsyncBulkApiHandlerTest { 45 | private static Logger LOG = LoggerFactory.getLogger(AsyncBulkApiHandlerTest.class); 46 | BulkConnection bulkConnection = null; 47 | AsyncBulkApiHandler bulkApiHandler = new AsyncBulkApiHandler(); 48 | JobInfo jobInfo = null; 49 | String jobId = null; 50 | List batchInfoList = null; 51 | List batchResults = null; 52 | String parentJobId = null; 53 | String[] apexClasses = new String[200]; 54 | 55 | @BeforeTest 56 | public void setup() { 57 | /* new CommandLineArgumentsTest().setup(); 58 | ConnectionHandler connHandler = ConnectionHandler.getConnectionHandlerInstance(); 59 | bulkConnection = connHandler.getBulkConnection(); 60 | PartnerConnection conn = connHandler.getConnection(); 61 | // populate the classfile names 62 | // String[] apexClasses = new String[200]; 63 | // {"01p300000000KcmAAE","01p300000000KcnAAE", "01pQ00000007axIIAQ", 64 | // "01pQ00000007axJIAQ", "01pQ00000007axKIAQ", 65 | // "01pQ00000007axLIAQ", "01pQ00000007axMIAQ","01pQ00000007axNIAQ"}; 66 | apexClasses = ApexClassFetcherUtils.constructTestClassesArray(conn);*/ 67 | 68 | } 69 | 70 | /*@Test(priority = 1) 71 | public void createJob() { 72 | try { 73 | ConnectorConfig connConfig = bulkConnection.getConfig(); 74 | LOG.info("Rest end point:" + connConfig.getRestEndpoint()); 75 | jobInfo = bulkApiHandler.createJob("ApexTestQueueItem", bulkConnection, OperationEnum.insert); 76 | jobId = jobInfo.getId(); 77 | // making sure JobInfo has been created 78 | Assert.assertTrue(jobId != null); 79 | } catch (AsyncApiException e) { 80 | ApexUnitUtils 81 | .shutDownWithDebugLog(e, "Caught exception while trying to create job: " 82 | + e.getMessage()); 83 | } 84 | } 85 | 86 | @Test(priority = 2) 87 | public void createBatchesForApexClasses() { 88 | batchInfoList = bulkApiHandler.createBatchesForApexClasses(bulkConnection, jobInfo, apexClasses); 89 | Assert.assertTrue(batchInfoList.size() > 0); 90 | } 91 | 92 | @Test(priority = 3) 93 | public void closeJob() { 94 | try { 95 | bulkApiHandler.closeJob(bulkConnection, jobId); 96 | // make sure closeJob closes Job with a given Job Id 97 | Assert.assertEquals(JobStateEnum.Closed, bulkConnection.getJobStatus(jobId).getState()); 98 | } catch (AsyncApiException e) { 99 | ApexUnitUtils 100 | .shutDownWithDebugLog(e, e.getMessage()); 101 | } 102 | } 103 | 104 | @Test(priority = 4) 105 | public void checkResults() { 106 | try { 107 | bulkApiHandler.awaitCompletion(bulkConnection, jobInfo, batchInfoList); 108 | batchResults = bulkApiHandler.checkResults(bulkConnection, jobInfo, batchInfoList); 109 | Assert.assertTrue(batchResults.size() > 0); 110 | } catch (AsyncApiException e) { 111 | ApexUnitUtils 112 | .shutDownWithDebugLog(e, "AsyncApiException encountered while fetching batch results using Bulk API. Exception thrown is: " 113 | + e); 114 | } catch (IOException e) { 115 | ApexUnitUtils 116 | .shutDownWithDebugLog(e, "IOException encountered. Unable to get Batch stream info from Bulk connection. Exception thrown is: " 117 | + e); 118 | } 119 | } 120 | 121 | @Test(priority = 5) 122 | public void getParentJobId() { 123 | parentJobId = bulkApiHandler.getParentJobIdForTestQueueItems(batchResults, 124 | ConnectionHandler.getConnectionHandlerInstance().getConnection()); 125 | LOG.info("ParentJobId in the test: " + parentJobId); 126 | Assert.assertNotNull(parentJobId); 127 | } 128 | 129 | @Test(priority = 6) 130 | public void fetchResultsFromParentJobId() { 131 | 132 | TestStatusPollerAndResultHandler queryPollerAndResultHandler = new TestStatusPollerAndResultHandler(); 133 | PartnerConnection conn = ConnectionHandler.getConnectionHandlerInstance().getConnection(); 134 | ApexReportBean[] apexReportBeans = queryPollerAndResultHandler.fetchResultsFromParentJobId(parentJobId, conn); 135 | // check status of wait method to make sure 136 | // fetchResultsFromParentJobId() method is completed 137 | boolean isTestsExecutionCompleted = queryPollerAndResultHandler.waitForTestsToComplete(parentJobId, conn); 138 | Assert.assertEquals(true, isTestsExecutionCompleted); 139 | Assert.assertTrue(apexReportBeans.length > 0); 140 | }*/ 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/testEngine/TestExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class for controlling the test execution flow in APexUnit 10 | * @author adarsh.ramakrishna@salesforce.com 11 | */ 12 | 13 | 14 | package com.sforce.cd.apexUnit.client.testEngine; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import com.sforce.async.BulkConnection; 24 | import com.sforce.cd.apexUnit.ApexUnitUtils; 25 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 26 | import com.sforce.cd.apexUnit.client.QueryConstructor; 27 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 28 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 29 | import com.sforce.cd.apexUnit.report.ApexReportBean; 30 | import com.sforce.soap.partner.PartnerConnection; 31 | import com.sforce.soap.partner.QueryResult; 32 | import com.sforce.soap.partner.sobject.SObject; 33 | import com.sforce.ws.ConnectionException; 34 | 35 | public class TestExecutor { 36 | private static Logger LOG = LoggerFactory.getLogger(TestExecutor.class); 37 | private static final int BATCH_SIZE = 200; 38 | 39 | public ApexReportBean[] testExecutionFlow() { 40 | 41 | ConnectionHandler connectionHandler = ConnectionHandler.getConnectionHandlerInstance(); 42 | PartnerConnection conn = connectionHandler.getConnection(); 43 | BulkConnection bulkConnection = null; 44 | AsyncBulkApiHandler bulkApiHandler = null; 45 | String[] testClassesInBatch = null; 46 | String parentJobId; 47 | ArrayList apexReportBean = null; 48 | ApexReportBean[] apexReportBeanArray = null; 49 | 50 | 51 | if (conn == null) { 52 | ApexUnitUtils.shutDownWithErrMsg("Unable to establish Connection with the org. Suspending the run.."); 53 | } 54 | 55 | String[] testClassesAsArray = ApexClassFetcherUtils.constructTestClassesArray(conn); 56 | if (LOG.isDebugEnabled()) { 57 | ApexClassFetcherUtils.logTheFetchedApexClasses(testClassesAsArray); 58 | } 59 | String soql = QueryConstructor.getQueryForApexClassInfo(processClassArrayForQuery(testClassesAsArray)); 60 | QueryResult queryresult = null; 61 | try { 62 | queryresult = conn.query(soql); 63 | } catch (ConnectionException e) { 64 | 65 | LOG.debug(e.getMessage()); 66 | } 67 | SObject []s= queryresult.getRecords(); 68 | SObject[] updateResult = new SObject[s.length]; 69 | int i =0; 70 | for (SObject sObject : s) { 71 | SObject obj = new SObject(); 72 | obj.setType("ApexTestQueueItem"); 73 | obj.setId(sObject.getId()); 74 | obj.setField("status", "Aborted"); 75 | updateResult[i++] = obj; 76 | } 77 | LOG.info("No of test classes running tests "+queryresult.getSize()); 78 | boolean submitTest = true; 79 | if(queryresult.getSize() != 0){ 80 | LOG.info("Test Reload "+ CommandLineArguments.isTestReload()); 81 | if(CommandLineArguments.isTestReload()){ 82 | 83 | try { 84 | conn.update(updateResult); 85 | } catch (ConnectionException e) { 86 | LOG.debug(e.getMessage()); 87 | } 88 | } 89 | else{ 90 | submitTest = false; 91 | } 92 | 93 | 94 | } 95 | 96 | if(!submitTest){ 97 | ApexUnitUtils.shutDownWithErrMsg("Test for these classes already running/enqueue at server..."); 98 | } 99 | else{ 100 | 101 | if (testClassesAsArray != null && testClassesAsArray.length > 0) { 102 | 103 | int numOfBatches = 0; 104 | int fromIndex = 0; 105 | int toIndex = 0; 106 | apexReportBean = new ArrayList(); 107 | bulkConnection = connectionHandler.getBulkConnection(); 108 | bulkApiHandler = new AsyncBulkApiHandler(); 109 | 110 | int lastSetOfClasses = testClassesAsArray.length % BATCH_SIZE; 111 | if (lastSetOfClasses == 0) { 112 | numOfBatches = testClassesAsArray.length / BATCH_SIZE; 113 | } else { 114 | numOfBatches = testClassesAsArray.length / BATCH_SIZE + 1; 115 | } 116 | 117 | for (int count = 0; count < numOfBatches; count++) { 118 | 119 | fromIndex = count * BATCH_SIZE; 120 | toIndex = (lastSetOfClasses != 0 && count == numOfBatches - 1) ? (toIndex + lastSetOfClasses) 121 | : (fromIndex + BATCH_SIZE); 122 | 123 | testClassesInBatch = Arrays.copyOfRange(testClassesAsArray, fromIndex, toIndex); 124 | parentJobId = bulkApiHandler.handleBulkApiFlow(conn, bulkConnection, testClassesInBatch); 125 | 126 | LOG.info("#####Parent JOB ID #####"+parentJobId); 127 | if (parentJobId != null) { 128 | LOG.info("Parent job ID for the submission of the test classes to the Force.com platform is: " 129 | + parentJobId); 130 | TestStatusPollerAndResultHandler queryPollerAndResultHandler = new TestStatusPollerAndResultHandler(); 131 | LOG.info( 132 | "############################# Now executing - Apex tests.. #############################"); 133 | apexReportBeanArray = queryPollerAndResultHandler.fetchResultsFromParentJobId(parentJobId, conn); 134 | if(apexReportBeanArray != null){ 135 | apexReportBean.addAll(Arrays.asList(apexReportBeanArray)); 136 | } 137 | 138 | } 139 | 140 | } 141 | 142 | } 143 | } 144 | return apexReportBean.toArray(new ApexReportBean[0]); 145 | 146 | } 147 | 148 | private String processClassArrayForQuery(String[] classesAsArray) { 149 | String queryString = ""; 150 | for (int i = 0; i < classesAsArray.length; i++) { 151 | queryString += "'" + classesAsArray[i] + "'"; 152 | queryString += ","; 153 | } 154 | if (queryString.length() > 1) { 155 | queryString = queryString.substring(0, queryString.length() - 1); 156 | } 157 | return queryString; 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/arguments/CommandLineArgumentsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package com.sforce.cd.apexUnit.arguments; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.testng.Assert; 13 | import org.testng.annotations.BeforeTest; 14 | import org.testng.annotations.Test; 15 | 16 | import com.beust.jcommander.JCommander; 17 | 18 | public class CommandLineArgumentsTest { 19 | private static Logger logs = LoggerFactory.getLogger(CommandLineArgumentsTest.class); 20 | CommandLineArguments cmdLineArgs = new CommandLineArguments(); 21 | // initialize test parameter values. 22 | // Change the below values to execute various positive/negative test cases 23 | 24 | private final String SERVER_ORG_LOGIN_URL_PARAMETER = System.getProperty(CommandLineArguments.ORG_LOGIN_URL); 25 | private final String ORG_USERNAME_PARAMETER = System.getProperty(CommandLineArguments.ORG_USERNAME); 26 | private final String ORG_PASSWORD_PARAMETER = System.getProperty(CommandLineArguments.ORG_PASSWORD); 27 | private final String CLIENT_ID = System.getProperty(CommandLineArguments.ORG_CLIENT_ID); 28 | private final String CLIENT_SECRET = System.getProperty(CommandLineArguments.ORG_CLIENT_SECRET); 29 | private final String MANIFEST_FILE_PARAMETER = System 30 | .getProperty(CommandLineArguments.MANIFEST_FILES_WITH_TEST_CLASS_NAMES_TO_EXECUTE); 31 | private final String CLASS_MANIFEST_FILE_PARAMETER = System 32 | .getProperty(CommandLineArguments.MANIFEST_FILES_WITH_SOURCE_CLASS_NAMES_FOR_CODE_COVERAGE_COMPUTATION); 33 | private final String TEST_PREFIX_PARAMETER = System 34 | .getProperty(CommandLineArguments.REGEX_FOR_SELECTING_TEST_CLASSES_TO_EXECUTE); 35 | private final String ORG_WIDE_CC_THRESHOLD_PARAMETER = System 36 | .getProperty(CommandLineArguments.ORG_WIDE_CODE_COVERAGE_THRESHOLD); 37 | private final String TEAM_CC_THRESHOLD_PARAMETER = System 38 | .getProperty(CommandLineArguments.TEAM_CODE_COVERAGE_THRESHOLD); 39 | private final String CLASS_PREFIX_PARAMETER = System 40 | .getProperty(CommandLineArguments.REGEX_FOR_SELECTING_SOURCE_CLASSES_FOR_CODE_COVERAGE_COMPUTATION); 41 | private final String MAX_TEST_EXEC_TIME_THRESHOLD = System 42 | .getProperty(CommandLineArguments.MAX_TEST_EXECUTION_TIME_THRESHOLD); 43 | 44 | @BeforeTest 45 | public void setup() { 46 | StringBuffer arguments = new StringBuffer(); 47 | 48 | arguments.append(CommandLineArguments.ORG_LOGIN_URL); 49 | arguments.append(appendSpaces(SERVER_ORG_LOGIN_URL_PARAMETER)); 50 | arguments.append(CommandLineArguments.ORG_USERNAME); 51 | arguments.append(appendSpaces(ORG_USERNAME_PARAMETER)); 52 | arguments.append(CommandLineArguments.ORG_PASSWORD); 53 | arguments.append(appendSpaces(ORG_PASSWORD_PARAMETER)); 54 | arguments.append(CommandLineArguments.MANIFEST_FILES_WITH_TEST_CLASS_NAMES_TO_EXECUTE); 55 | arguments.append(appendSpaces(MANIFEST_FILE_PARAMETER)); 56 | arguments.append(CommandLineArguments.MANIFEST_FILES_WITH_SOURCE_CLASS_NAMES_FOR_CODE_COVERAGE_COMPUTATION); 57 | arguments.append(appendSpaces(CLASS_MANIFEST_FILE_PARAMETER)); 58 | arguments.append(CommandLineArguments.REGEX_FOR_SELECTING_TEST_CLASSES_TO_EXECUTE); 59 | arguments.append(appendSpaces(TEST_PREFIX_PARAMETER)); 60 | arguments.append(CommandLineArguments.ORG_WIDE_CODE_COVERAGE_THRESHOLD); 61 | arguments.append(appendSpaces(ORG_WIDE_CC_THRESHOLD_PARAMETER)); 62 | arguments.append(CommandLineArguments.TEAM_CODE_COVERAGE_THRESHOLD); 63 | arguments.append(appendSpaces(TEAM_CC_THRESHOLD_PARAMETER)); 64 | arguments.append(CommandLineArguments.REGEX_FOR_SELECTING_SOURCE_CLASSES_FOR_CODE_COVERAGE_COMPUTATION); 65 | arguments.append(appendSpaces(CLASS_PREFIX_PARAMETER)); 66 | arguments.append(CommandLineArguments.MAX_TEST_EXECUTION_TIME_THRESHOLD); 67 | arguments.append(appendSpaces(MAX_TEST_EXEC_TIME_THRESHOLD)); 68 | arguments.append(CommandLineArguments.ORG_CLIENT_ID); 69 | arguments.append(appendSpaces(CLIENT_ID)); 70 | arguments.append(CommandLineArguments.ORG_CLIENT_SECRET); 71 | arguments.append(appendSpaces(CLIENT_SECRET)); 72 | String[] args = arguments.toString().split(" "); 73 | 74 | //JCommander jcommander = new JCommander(cmdLineArgs, args); 75 | } 76 | 77 | /* @Test 78 | public void getOrgWideCodeCoverageThreshold() { 79 | Assert.assertEquals(CommandLineArguments.getOrgWideCodeCoverageThreshold().intValue(), 80 | Integer.parseInt(ORG_WIDE_CC_THRESHOLD_PARAMETER)); 81 | } 82 | 83 | @Test 84 | public void getManifestFileLoc() { 85 | Assert.assertEquals(CommandLineArguments.getTestManifestFiles(), MANIFEST_FILE_PARAMETER); 86 | } 87 | 88 | @Test 89 | public void getClassManifestFileLoc() { 90 | Assert.assertEquals(CommandLineArguments.getClassManifestFiles(), CLASS_MANIFEST_FILE_PARAMETER); 91 | } 92 | 93 | public void getPassword() { 94 | Assert.assertEquals(CommandLineArguments.getPassword(), ORG_PASSWORD_PARAMETER); 95 | 96 | } 97 | 98 | @Test 99 | public void getTeamCodeCoverageThreshold() { 100 | Assert.assertEquals(CommandLineArguments.getTeamCodeCoverageThreshold().intValue(), 101 | Integer.parseInt(TEAM_CC_THRESHOLD_PARAMETER)); 102 | } 103 | 104 | @Test 105 | public void getTestPrefix() { 106 | Assert.assertEquals(CommandLineArguments.getTestRegex(), TEST_PREFIX_PARAMETER); 107 | } 108 | 109 | @Test 110 | public void getUrl() { 111 | Assert.assertEquals(CommandLineArguments.getOrgUrl(), SERVER_ORG_LOGIN_URL_PARAMETER); 112 | } 113 | 114 | @Test 115 | public void getUsername() { 116 | Assert.assertEquals(CommandLineArguments.getUsername(), ORG_USERNAME_PARAMETER); 117 | } 118 | 119 | @Test 120 | public void getClassPrefix() { 121 | Assert.assertEquals(CommandLineArguments.getSourceRegex(), CLASS_PREFIX_PARAMETER); 122 | } 123 | 124 | @Test 125 | public void getClientId() { 126 | Assert.assertEquals(CommandLineArguments.getClientId(), CLIENT_ID); 127 | } 128 | 129 | @Test 130 | public void getClientSecret() { 131 | Assert.assertEquals(CommandLineArguments.getClientSecret(), CLIENT_SECRET); 132 | } 133 | */ 134 | private String appendSpaces(String input) { 135 | if (input != null && !input.isEmpty()) { 136 | return " " + input.toString() + " "; 137 | } 138 | return " "; 139 | } 140 | 141 | @Test 142 | public void tempTest1(){ 143 | Assert.assertNotNull("temp"); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 11 | 4.0.0 12 | 13 | com.sforce.cd.ApexUnit 14 | ApexUnit-core 15 | 4.0.0 16 | ApexUnit 17 | Apex Unit v 2.0 with enhanced metrics and advanced features 18 | 19 | 20 | 3.0.4 21 | 22 | 25 | 26 | 27 | 28 | true 29 | daily 30 | warn 31 | 32 | 33 | true 34 | never 35 | warn 36 | 37 | oss-nexus-releases 38 | oss sonatype nexus 39 | https://oss.sonatype.org/content/repositories/releases 40 | 41 | 42 | 43 | deploy 44 | src/main/java 45 | src/test/java 46 | 47 | 48 | src/main/resources 49 | 50 | 51 | 52 | 53 | src/test/resources 54 | 55 | 56 | 57 | 58 | org.apache.maven.archetype 59 | archetype-packaging 60 | 2.3 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-archetype-plugin 68 | 2.3 69 | 70 | 71 | org.codehaus.mojo 72 | cobertura-maven-plugin 73 | 2.5.1 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-shade-plugin 81 | 2.3 82 | 83 | 84 | package 85 | 86 | shade 87 | 88 | 89 | false 90 | 91 | 93 | com.sforce.cd.apexUnit.ApexUnitRunner 94 | 95 | 97 | .txt 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | maven-surefire-plugin 106 | 2.18.1 107 | 108 | src/main/resources/config.properties 109 | 110 | 111 | 112 | org.codehaus.mojo 113 | exec-maven-plugin 114 | 1.2.1 115 | 116 | 117 | 118 | java 119 | 120 | 121 | 122 | 123 | com.sforce.cd.apexUnit.ApexUnitRunner 124 | 125 | 126 | 127 | org.codehaus.mojo 128 | license-maven-plugin 129 | 1.8 130 | 131 | 132 | 133 | 134 | 135 | org.slf4j 136 | slf4j-simple 137 | 1.7.5 138 | 139 | 140 | org.testng 141 | testng 142 | 6.8.7 143 | 144 | 145 | com.beust 146 | jcommander 147 | 1.35 148 | 149 | 150 | 151 | org.daisy.libs 152 | commons-httpclient 153 | 3.1.0 154 | 155 | 156 | com.google.code.gson 157 | gson 158 | 2.2.2 159 | 160 | 161 | com.force.api 162 | force-partner-api 163 | 37.0.3 164 | 165 | 166 | com.force.api 167 | force-wsc 168 | 37.0.3 169 | 170 | 171 | commons-lang 172 | commons-lang 173 | 2.3 174 | 175 | 176 | org.apache.commons 177 | commons-io 178 | 1.3.2 179 | 180 | 181 | org.jdom 182 | jdom 183 | 1.1 184 | 185 | 186 | com.googlecode.json-simple 187 | json-simple 188 | 1.1 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /src/test/java/com/sforce/cd/apexUnit/fileReader/ApexManifestFileReaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * @author adarsh.ramakrishna@salesforce.com 10 | */ 11 | package com.sforce.cd.apexUnit.fileReader; 12 | 13 | import java.io.BufferedWriter; 14 | import java.io.File; 15 | import java.io.FileWriter; 16 | import java.io.IOException; 17 | import java.net.URISyntaxException; 18 | 19 | import org.apache.commons.io.FileUtils; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.testng.annotations.AfterTest; 23 | import org.testng.annotations.BeforeTest; 24 | 25 | import com.sforce.cd.apexUnit.ApexUnitUtils; 26 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 27 | import com.sforce.cd.apexUnit.client.fileReader.ApexManifestFileReader; 28 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 29 | import com.sforce.soap.partner.PartnerConnection; 30 | import com.sforce.soap.partner.QueryResult; 31 | import com.sforce.soap.partner.sobject.SObject; 32 | import com.sforce.ws.ConnectionException; 33 | 34 | public class ApexManifestFileReaderTest { 35 | private final static Logger LOG = LoggerFactory.getLogger(ApexManifestFileReaderTest.class); 36 | 37 | private String workingDir = System.getProperty("user.dir") + System.getProperty("file.separator") + "MockTestFiles"; 38 | String fileName = "ManifestFile.txt"; 39 | String manifestFiles = "ManifestFile1.txt,ManifestFile2.txt,ManifestFile3.txt"; 40 | ApexClassFetcherUtils apexClassFetcher = new ApexClassFetcherUtils(); 41 | int limit = 10; 42 | String[] apexClasses = new String[limit]; 43 | ApexManifestFileReader apexManifestFileReader = new ApexManifestFileReader(true); 44 | String testClassesAsString = null; 45 | 46 | @BeforeTest 47 | public void setup() throws IOException, URISyntaxException { 48 | String soql = ""; 49 | // writing to a file in the current working directory 50 | /**LOG.info("Working directory : " + workingDir); 51 | PartnerConnection connection = ConnectionHandler.getConnectionHandlerInstance().getConnection(); 52 | File dir = new File(workingDir); 53 | dir.mkdirs(); 54 | File file = new File(dir, fileName); 55 | soql = "SELECT Id , Name FROM ApexClass LIMIT 10"; 56 | QueryResult queryResult = null; 57 | try { 58 | queryResult = connection.query(soql); 59 | LOG.info("soql: " + soql); 60 | } catch (ConnectionException e) { 61 | ApexUnitUtils 62 | .shutDownWithDebugLog(e, "Connection Exception encountered when trying to query : "+ soql + " \n The connection exception description says : " + e.getMessage()); 63 | } 64 | if (queryResult != null) { 65 | testClassesAsString = fetchApexClassesAsString(queryResult); 66 | LOG.info("testClassesAsString: " + testClassesAsString); 67 | } 68 | 69 | try { 70 | 71 | FileWriter fw = new FileWriter(file); 72 | BufferedWriter bw = new BufferedWriter(fw); 73 | bw.write(testClassesAsString); 74 | bw.close(); 75 | String cvsSplitBy = ","; 76 | String[] manifestFilesAsArray = manifestFiles.split(cvsSplitBy); 77 | for (String manifestFile : manifestFilesAsArray) { 78 | LOG.info("Creating Manifest file : " + manifestFile); 79 | File tmpFile = new File(dir, manifestFile); 80 | FileWriter fileWriter = new FileWriter(tmpFile); 81 | BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); 82 | bufferedWriter.write(testClassesAsString); 83 | bufferedWriter.close(); 84 | } 85 | } catch (IOException e) { 86 | ApexUnitUtils 87 | .shutDownWithDebugLog(e, "IO Exception caught \n"); 88 | }*/ 89 | 90 | } 91 | 92 | private String fetchApexClassesAsString(QueryResult queryResult) { 93 | String apexClassesAsString = null; 94 | int arrayLength = 0; 95 | String apexClassId = ""; 96 | String apexClassName = ""; 97 | 98 | if (queryResult.getDone()) { 99 | SObject[] sObjects = queryResult.getRecords(); 100 | if (sObjects != null) { 101 | LOG.debug("Fetched the classes:"); 102 | for (SObject sobject : sObjects) { 103 | apexClassName = sobject.getField("Name").toString(); 104 | apexClassesAsString += apexClassName; 105 | apexClassesAsString += "\n"; 106 | apexClassId = sobject.getField("Id").toString(); 107 | apexClasses[arrayLength++] = apexClassId; 108 | ApexClassFetcherUtils.apexClassMap.put(apexClassId, apexClassName); 109 | } 110 | return apexClassesAsString; 111 | } 112 | } 113 | return null; 114 | } 115 | 116 | // @Test 117 | /* 118 | * public void readInputStream() throws FileNotFoundException { String 119 | * filePath = workingDir +"/" + fileName; File file = new File(filePath); 120 | * InputStream inStr = new FileInputStream(file); String[] testClasses = 121 | * null; try { testClasses = apexManifestFileReader.readInputStream(inStr); 122 | * } catch (IOException e) { LOG.error( 123 | * "IOException while trying to read the manifest file " + file); if 124 | * (LOG.isDebugEnabled()) { e.printStackTrace(); } } StringBuffer 125 | * testClassesStrBuffer = new StringBuffer(); for(String testClass : 126 | * testClasses){ testClassesStrBuffer.append(testClass); 127 | * testClassesStrBuffer.append("\n"); } 128 | * ApexClassFetcherUtils.logTheFetchedApexClasses(testClasses); 129 | * Assert.assertTrue(ApexClassFetcherUtils.apexClassMap.containsKey( 130 | * apexClasses[0])); 131 | * 132 | * } 133 | * 134 | * // @Test public void getTestClasses() { LOG.info( 135 | * "testing getTestClasses() " + manifestFiles); String[] apexClassesStrArr 136 | * = apexManifestFileReader.getTestClasses(manifestFiles); 137 | * if(apexClassesStrArr != null && apexClassesStrArr.length > 0) { LOG.info( 138 | * "apexClassesStrArr.length: " + apexClassesStrArr.length); 139 | * Assert.assertTrue(apexClassesStrArr.length > 0); } } 140 | */ 141 | 142 | @AfterTest 143 | public void cleanUpTestFiles() { 144 | /*String testFilesDirPath = System.getProperty("user.dir") + System.getProperty("file.separator") 145 | + "MockTestFiles"; 146 | File testFilesDir = new File(testFilesDirPath); 147 | if (testFilesDir.exists()) { 148 | try { 149 | FileUtils.deleteDirectory(testFilesDir); 150 | } catch (IOException e) { 151 | ApexUnitUtils.shutDownWithDebugLog(e, "IO Exception encountered while deleting the test files directory"); 152 | } 153 | LOG.info("Test files directory deleted"); 154 | } else { 155 | LOG.info("Test files directory does not exist; hence not deleted"); 156 | }*/ 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/fileReader/ApexManifestFileReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to read manifest files containing Apex class names 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.client.fileReader; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.DataInputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.HashMap; 25 | import java.util.HashSet; 26 | import java.util.Map; 27 | import java.util.Set; 28 | 29 | import org.apache.commons.lang.ArrayUtils; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import com.sforce.cd.apexUnit.ApexUnitUtils; 34 | import com.sforce.cd.apexUnit.client.QueryConstructor; 35 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 36 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 37 | 38 | public class ApexManifestFileReader { 39 | private static Logger LOG = LoggerFactory.getLogger(ApexManifestFileReader.class); 40 | public static ArrayList nonExistantApexClassEntries = new ArrayList(); 41 | private boolean includeTriggers; 42 | 43 | public ApexManifestFileReader(boolean includeTriggers) { 44 | this.includeTriggers = includeTriggers; 45 | } 46 | 47 | public String[] fetchClassNamesFromManifestFiles(String files) { 48 | String[] apexClassesStrArr = null; 49 | String[] apexClassesStrArrForManifest = null; 50 | LOG.info("Reading from Manifest files: " + files); 51 | String[] manifestFiles = files.split(","); 52 | 53 | for (String file : manifestFiles) { 54 | LOG.info("Reading Manifest file from location : " + file); 55 | InputStream inStr; 56 | try { 57 | inStr = this.getClass().getClassLoader().getResourceAsStream(file); 58 | if (inStr != null) { 59 | apexClassesStrArrForManifest = readInputStreamAndConstructClassArray(inStr); 60 | } else { 61 | ApexUnitUtils.shutDownWithErrMsg( 62 | "Unable to find the file " + file + " in the src->main->resources folder"); 63 | } 64 | } catch (IOException e) { 65 | ApexUnitUtils.shutDownWithDebugLog(e, "IOException while trying to read the manifest file " + file); 66 | } 67 | if (apexClassesStrArrForManifest != null) { 68 | apexClassesStrArr = (String[]) ArrayUtils.addAll(apexClassesStrArr, apexClassesStrArrForManifest); 69 | } else { 70 | LOG.warn("Given manifest file " + file 71 | + " contains invalid/no test classes. 0 Apex test class id's returned"); 72 | } 73 | } 74 | Set uniqueSetOfClasses = new HashSet(); 75 | if (apexClassesStrArr != null && apexClassesStrArr.length > 0) { 76 | for (String apexClass : apexClassesStrArr) { 77 | if (!uniqueSetOfClasses.add(apexClass)) { 78 | LOG.warn("Duplicate entry found across manifest files for : " 79 | + ApexClassFetcherUtils.apexClassMap.get(apexClass) 80 | + " . Skipping multiple execution/code coverage computation of this test class/source class"); 81 | } 82 | } 83 | } 84 | String[] uniqueClassesAsArray = uniqueSetOfClasses.toArray(new String[uniqueSetOfClasses.size()]); 85 | if (LOG.isDebugEnabled()) { 86 | ApexClassFetcherUtils.logTheFetchedApexClasses(apexClassesStrArr); 87 | } 88 | return uniqueClassesAsArray; 89 | } 90 | 91 | // Split up the method for testability 92 | private String[] readInputStreamAndConstructClassArray(InputStream inStr) throws IOException { 93 | String[] testClassesAsArray = null; 94 | ArrayList testClassList = new ArrayList(); 95 | 96 | LOG.debug("Input stream: " + inStr); 97 | DataInputStream dataIS = new DataInputStream(inStr); 98 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(dataIS)); 99 | String strLine = null; 100 | String newline = System.getProperty("line.separator"); 101 | 102 | while ((strLine = bufferedReader.readLine()) != null) { 103 | if (!newline.equals(strLine) && !strLine.equals("") && strLine.length() > 0) { 104 | LOG.debug("The line says .... - " + strLine); 105 | insertIntoTestClassesArray(strLine, testClassList); 106 | } 107 | } 108 | dataIS.close(); 109 | 110 | Object[] apexClassesObjArr = testClassList.toArray(); 111 | testClassesAsArray = (Arrays.copyOf(apexClassesObjArr, apexClassesObjArr.length, String[].class)); 112 | if (LOG.isDebugEnabled()) { 113 | ApexClassFetcherUtils.logTheFetchedApexClasses(testClassesAsArray); 114 | } 115 | return testClassesAsArray; 116 | } 117 | 118 | /* 119 | * inserts the classname from manifest file to the test class array 120 | * 121 | * @param strLine - String type - classname provided on manifest file 122 | * @param testClassList - ArrayList of String - test classes saved so far 123 | */ 124 | private void insertIntoTestClassesArray(String strLine, ArrayList testClassList){ 125 | String tempTestClassId = null; 126 | Map namespaceAndName = new HashMap(); 127 | namespaceAndName.put("name",strLine); 128 | 129 | String soql = QueryConstructor.generateQueryToFetchApexClass(namespaceAndName.get("namespace"), 130 | namespaceAndName.get("name")); 131 | // query using WSC 132 | tempTestClassId = ApexClassFetcherUtils.fetchAndAddToMapApexClassIdBasedOnName( 133 | ConnectionHandler.getConnectionHandlerInstance().getConnection(), soql); 134 | LOG.debug("tempTestClassId: " + tempTestClassId); 135 | 136 | //triggers are included only for code coverage and not for tests to avoid exception by the platform 137 | if (tempTestClassId == null && includeTriggers) { 138 | // look if the given class name is a trigger if its not ApexClass 139 | String soqlForTrigger = QueryConstructor.generateQueryToFetchApexTrigger(namespaceAndName.get("namespace"), 140 | namespaceAndName.get("name")); 141 | // query using WSC 142 | tempTestClassId = ApexClassFetcherUtils.fetchAndAddToMapApexClassIdBasedOnName( 143 | ConnectionHandler.getConnectionHandlerInstance().getConnection(), soqlForTrigger); 144 | LOG.debug("tempTestClassId(TriggerId: " + tempTestClassId); 145 | } 146 | if (tempTestClassId != null) { 147 | if (!testClassList.contains(tempTestClassId)) { 148 | testClassList.add(tempTestClassId); 149 | ApexClassFetcherUtils.apexClassMap.put(tempTestClassId, strLine); 150 | } else { 151 | LOG.warn("Duplicate entry found in manifest file for : " + strLine 152 | + " . Skipping multiple execution/code coverage computation of this test class/source class"); 153 | ApexClassFetcherUtils.duplicateApexClassMap.put(tempTestClassId, strLine); 154 | } 155 | 156 | } else { 157 | LOG.warn("The class " + strLine + " does not exist in the org."); 158 | if (!nonExistantApexClassEntries.contains(strLine)) { 159 | nonExistantApexClassEntries.add(strLine); 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/arguments/CommandLineArguments.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * CommandLineArguments class used JCommander tool for accepting, validating and assigning to the 10 | * command line arguments for the ApexUnit tool 11 | * The class exposes getter methods for global access of the command line arguments in the tool 12 | * 13 | * @author adarsh.ramakrishna@salesforce.com 14 | */ 15 | 16 | package com.sforce.cd.apexUnit.arguments; 17 | 18 | import com.beust.jcommander.Parameter; 19 | 20 | public class CommandLineArguments { 21 | /* 22 | * Static variables that define the command line options 23 | */ 24 | public static final String ORG_LOGIN_URL = "-org.login.url"; 25 | public static final String ORG_USERNAME = "-org.username"; 26 | public static final String ORG_PASSWORD = "-org.password"; 27 | public static final String MANIFEST_FILES_WITH_TEST_CLASS_NAMES_TO_EXECUTE = "-manifest.files.with.test.class.names.to.execute"; 28 | public static final String MANIFEST_FILES_WITH_SOURCE_CLASS_NAMES_FOR_CODE_COVERAGE_COMPUTATION = "-manifest.files.with.source.class.names.for.code.coverage.computation"; 29 | public static final String REGEX_FOR_SELECTING_TEST_CLASSES_TO_EXECUTE = "-regex.for.selecting.test.classes.to.execute"; 30 | public static final String REGEX_FOR_SELECTING_SOURCE_CLASSES_FOR_CODE_COVERAGE_COMPUTATION = "-regex.for.selecting.source.classes.for.code.coverage.computation"; 31 | public static final String ORG_WIDE_CODE_COVERAGE_THRESHOLD = "-org.wide.code.coverage.threshold"; 32 | public static final String TEAM_CODE_COVERAGE_THRESHOLD = "-team.code.coverage.threshold"; 33 | public static final String MAX_TEST_EXECUTION_TIME_THRESHOLD = "-max.test.execution.time.threshold"; 34 | public static final String ORG_CLIENT_ID = "-org.client.id"; 35 | public static final String ORG_CLIENT_SECRET = "-org.client.secret"; 36 | public static final String PROXY_HOST = "-proxy.host"; 37 | public static final String PROXY_PORT = "-proxy.port"; 38 | public static final String TEST_RELOAD = "-test.reload"; 39 | 40 | public static final String HELP = "-help"; 41 | 42 | /* 43 | * Define Parameters using JCommander framework 44 | */ 45 | @Parameter(names = ORG_LOGIN_URL, description = "Login URL for the org", required = true, validateWith = URLValidator.class) 46 | static private String orgUrl = System.getProperty("SERVER_URL_PARAMETER"); 47 | @Parameter(names = ORG_USERNAME, description = "Username for the org", required = true) 48 | static private String username = System.getProperty("SERVER_USERNAME_PARAMETER"); 49 | @Parameter(names = ORG_PASSWORD, description = "Password corresponding to the username for the org", required = true) 50 | static private String password = System.getProperty("SERVER_PASSWORD_PARAMETER"); 51 | @Parameter(names = MANIFEST_FILES_WITH_TEST_CLASS_NAMES_TO_EXECUTE, description = "Manifest files containing the list of test classes to be executed", variableArity = true) 52 | static private String testManifestFiles = null; 53 | @Parameter(names = MANIFEST_FILES_WITH_SOURCE_CLASS_NAMES_FOR_CODE_COVERAGE_COMPUTATION, description = "Manifest files containing the list of Apex classes for which code coverage" 54 | + " is to be computed", variableArity = true) 55 | static private String classManifestFiles = null; 56 | @Parameter(names = REGEX_FOR_SELECTING_TEST_CLASSES_TO_EXECUTE, description = "The test regex used by the team for the apex test classes. " 57 | + "All tests beginning with this parameter in the org will be selected to run", variableArity = true) 58 | static private String testRegex; 59 | @Parameter(names = REGEX_FOR_SELECTING_SOURCE_CLASSES_FOR_CODE_COVERAGE_COMPUTATION, description = "The source regex used by the team for the apex source classes. " 60 | + "All classes beginning with this parameter in the org will be used to compute team code coverage", variableArity = true) 61 | static private String sourceRegex; 62 | @Parameter(names = ORG_WIDE_CODE_COVERAGE_THRESHOLD, description = "Org wide minimum code coverage required to meet the code coverage standards", validateWith = PercentageInputValidator.class, variableArity = true) 63 | static private Integer orgWideCodeCoverageThreshold = 75; 64 | @Parameter(names = TEAM_CODE_COVERAGE_THRESHOLD, description = "Team wide minimum code coverage required to meet the code coverage standards", validateWith = PercentageInputValidator.class, variableArity = true) 65 | static private Integer teamCodeCoverageThreshold = 75; 66 | @Parameter(names = MAX_TEST_EXECUTION_TIME_THRESHOLD, description = "Maximum execution time(in minutes) for a test before it gets aborted", validateWith = PositiveIntegerValidator.class, variableArity = true) 67 | static private Integer maxTestExecTimeThreshold; 68 | @Parameter(names = ORG_CLIENT_ID, description = "Client ID associated with the org. " 69 | + "Steps to confirgure/retrieve client ID/Secret: " 70 | + "https://www.salesforce.com/us/developer/docs/api_rest/Content/quickstart_oauth.htm", required = true) 71 | static private String clientId; 72 | @Parameter(names = ORG_CLIENT_SECRET, description = "Client Secret associated with the org.", required = true) 73 | static private String clientSecret; 74 | @Parameter(names = PROXY_HOST, description = "Proxy host if required for access.", required = false) 75 | static private String proxyHost; 76 | @Parameter(names = PROXY_PORT, description = "Proxy port if required for access.", validateWith = PositiveIntegerValidator.class, required = false) 77 | static private Integer proxyPort; 78 | @Parameter(names = HELP, help = true, description = "Displays options available for running this application") 79 | static private boolean help; 80 | @Parameter(names = TEST_RELOAD, description = "Want to reload test if same class changes submitted again.", arity=1) 81 | static private boolean testReload; 82 | 83 | /* 84 | * Static getter methods for each of the CLI parameter 85 | */ 86 | public static String getOrgUrl() { 87 | return orgUrl; 88 | } 89 | 90 | public static String getUsername() { 91 | return username; 92 | } 93 | 94 | public static String getPassword() { 95 | return password; 96 | } 97 | 98 | public static String getTestManifestFiles() { 99 | return testManifestFiles; 100 | } 101 | 102 | public static String getClassManifestFiles() { return classManifestFiles; } 103 | 104 | public static String getTestRegex() { 105 | return testRegex; 106 | } 107 | 108 | public static String getSourceRegex() { 109 | return sourceRegex; 110 | } 111 | 112 | public static Integer getOrgWideCodeCoverageThreshold() { 113 | return orgWideCodeCoverageThreshold; 114 | } 115 | 116 | public static Integer getTeamCodeCoverageThreshold() { 117 | return teamCodeCoverageThreshold; 118 | } 119 | 120 | public static Integer getMaxTestExecTimeThreshold() { 121 | return maxTestExecTimeThreshold; 122 | } 123 | 124 | public static String getClientId() { 125 | return clientId; 126 | } 127 | 128 | public static void setClientId(String clientId) { 129 | CommandLineArguments.clientId = clientId; 130 | } 131 | 132 | public static String getClientSecret() { 133 | return clientSecret; 134 | } 135 | 136 | public static String getProxyHost() { 137 | return proxyHost; 138 | } 139 | 140 | public static Integer getProxyPort() { 141 | return proxyPort; 142 | } 143 | 144 | public static void setClientSecret(String clientSecret) { 145 | CommandLineArguments.clientSecret = clientSecret; 146 | } 147 | 148 | public static boolean isHelp() { 149 | return help; 150 | } 151 | 152 | public static boolean isTestReload() { 153 | return testReload; 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/connection/ConnectionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class that creates and handles the connection with the org using salesforce wsc api's 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | package com.sforce.cd.apexUnit.client.connection; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.Properties; 19 | 20 | import org.apache.commons.lang.builder.ReflectionToStringBuilder; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import com.sforce.async.AsyncApiException; 25 | import com.sforce.async.BulkConnection; 26 | import com.sforce.cd.apexUnit.ApexUnitUtils; 27 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 28 | import com.sforce.cd.apexUnit.arguments.PositiveIntegerValidator; 29 | import com.sforce.soap.partner.Connector; 30 | import com.sforce.soap.partner.PartnerConnection; 31 | import com.sforce.ws.ConnectionException; 32 | import com.sforce.ws.ConnectorConfig; 33 | 34 | public class ConnectionHandler { 35 | private static ConnectionHandler connectionHandler = null; 36 | private static Logger LOG = LoggerFactory.getLogger(ConnectionHandler.class); 37 | Properties prop = new Properties(); 38 | String propFileName = "config.properties"; 39 | public static int MAX_TIME_OUT_IN_MS_INT; 40 | public static String SUPPORTED_VERSION = System.getProperty("API_VERSION"); 41 | private String MAX_TIME_OUT_IN_MS = System.getProperty("MAX_TIME_OUT_IN_MS"); 42 | 43 | private String sessionIdFromConnectorConfig = null; 44 | PartnerConnection connection = null; 45 | BulkConnection bulkConnection = null; 46 | /* 47 | * Constructor for ConnectionHandler 48 | * Initialize SUPPORTED_VERSION variable from property file 49 | * TODO fetch SUPPORTED_VERSION from the org(by querying?) 50 | */ 51 | private ConnectionHandler() { 52 | // execute below code Only when System.getProperty() call doesn't 53 | // function 54 | if (SUPPORTED_VERSION == null || MAX_TIME_OUT_IN_MS == null) { 55 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(propFileName); 56 | if (inputStream != null) { 57 | try { 58 | prop.load(inputStream); 59 | } catch (IOException e) { 60 | ApexUnitUtils.shutDownWithDebugLog(e, "Error while trying to load the property file " 61 | + propFileName 62 | + " Unable to establish Connection with the org. Suspending the run.."); 63 | } 64 | } 65 | SUPPORTED_VERSION = prop.getProperty("API_VERSION"); 66 | MAX_TIME_OUT_IN_MS = prop.getProperty("MAX_TIME_OUT_IN_MS"); 67 | } 68 | PositiveIntegerValidator positiveIntegerValidator = new PositiveIntegerValidator(); 69 | positiveIntegerValidator.validate("MAX_TIME_OUT_IN_MS", MAX_TIME_OUT_IN_MS); 70 | try { 71 | MAX_TIME_OUT_IN_MS_INT = Integer.parseInt(MAX_TIME_OUT_IN_MS); 72 | } catch (NumberFormatException nfe) { 73 | ApexUnitUtils.shutDownWithErrMsg( 74 | "Invalid value for MAX_TIME_OUT_IN_MS in the property file. Only positive integers are allowed"); 75 | } 76 | } 77 | 78 | // singleton pattern.. Ensures the class has only one common instance of 79 | // connectionHandler and provides global access point 80 | // TODO Comments by Vamshi: 81 | // "In that case this is perfect use case of builder design pattern. 82 | // Following such standards will eliminate these questions for any new 83 | // person to 84 | // understand the intent of your flow" 85 | public static ConnectionHandler getConnectionHandlerInstance() { 86 | if (connectionHandler == null) { 87 | connectionHandler = new ConnectionHandler(); 88 | } 89 | return connectionHandler; 90 | 91 | } 92 | 93 | /* 94 | * create partner connection using the wsc connector 95 | * 96 | * @return connection - instance of PartnerConnection 97 | */ 98 | 99 | private PartnerConnection createConnection() { 100 | if (connection == null) { 101 | PartnerConnectionConnectorConfig pcConnectorConfig = new PartnerConnectionConnectorConfig(); 102 | ConnectorConfig config = pcConnectorConfig.createConfig(); 103 | 104 | LOG.debug("creating connection for : " + CommandLineArguments.getUsername() + " " 105 | + CommandLineArguments.getOrgUrl() + " " 106 | + config.getUsername() + " " + config.getAuthEndpoint()); 107 | try { 108 | connection = Connector.newConnection(config); 109 | setSessionIdFromConnectorConfig(config); 110 | LOG.debug("Partner Connection established with the org!! \n SESSION ID IN createPartnerConn: " 111 | + sessionIdFromConnectorConfig); 112 | } catch (ConnectionException connEx) { 113 | ApexUnitUtils.shutDownWithDebugLog(connEx, ConnectionHandler 114 | .logConnectionException(connEx, connection)); 115 | } 116 | } 117 | return connection; 118 | } 119 | 120 | /* 121 | * method to retrieve sessionId from connector config 122 | * Used for initializing bulk connection 123 | * 124 | * @return sessionIdFromConnectorConfig = string representing session ID 125 | */ 126 | 127 | public String getSessionIdFromConnectorConfig() { 128 | // if sessionId is null, in all probability connection is null(not 129 | // created) 130 | if (sessionIdFromConnectorConfig == null && connection == null) { 131 | // createConnection() call creates connection and sets the 132 | // sessionId 133 | connection = createConnection(); 134 | } 135 | return sessionIdFromConnectorConfig; 136 | } 137 | 138 | /* 139 | * set session id from connection config 140 | */ 141 | public void setSessionIdFromConnectorConfig(ConnectorConfig config) { 142 | sessionIdFromConnectorConfig = config.getSessionId(); 143 | } 144 | 145 | // BulkConnection instance is the base for using the Bulk API. 146 | // The instance can be reused for the rest of the application life span. 147 | private BulkConnection createBulkConnection() { 148 | 149 | BulkConnectionConnectorConfig bcConnectorConfig = new BulkConnectionConnectorConfig(); 150 | ConnectorConfig config = bcConnectorConfig.createConfig(); 151 | try { 152 | bulkConnection = new BulkConnection(config); 153 | LOG.info("Bulk connection established."); 154 | } catch (AsyncApiException e) { 155 | ApexUnitUtils 156 | .shutDownWithDebugLog(e, "Caught AsyncApiException exception while trying to deal with bulk connection: " 157 | + e.getMessage()); 158 | } 159 | return bulkConnection; 160 | } 161 | 162 | public PartnerConnection getConnection() { 163 | if (connection == null) { 164 | connection = createConnection(); 165 | } 166 | return connection; 167 | } 168 | 169 | public void setConnection(PartnerConnection connection) { 170 | this.connection = connection; 171 | } 172 | 173 | public BulkConnection getBulkConnection() { 174 | if (bulkConnection == null) { 175 | createBulkConnection(); 176 | } 177 | return bulkConnection; 178 | } 179 | 180 | public void setBulkConnection(BulkConnection bulkConnection) { 181 | this.bulkConnection = bulkConnection; 182 | } 183 | 184 | public static String logConnectionException(ConnectionException connEx, PartnerConnection conn) { 185 | return logConnectionException(connEx, conn, null); 186 | } 187 | 188 | public static String logConnectionException(ConnectionException connEx, PartnerConnection conn, String soql) { 189 | StringBuffer returnString = new StringBuffer("Connection Exception encountered "); 190 | if (null != soql) { 191 | returnString.append("when trying to query : " + soql); 192 | } 193 | returnString.append(" The connection exception description says : " + connEx.getMessage()); 194 | returnString.append(" Object dump for the Connection object: " + ReflectionToStringBuilder.toString(conn)); 195 | 196 | return returnString.toString(); 197 | 198 | } 199 | 200 | } 201 | 202 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/ApexUnitRunner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * ApexUnitRunner serves as entry point to the ApexUnit 2.x tool 10 | * ApexUnit 2.x is test framework for Force.com platform and has two major functinalities: 11 | * Test execution: Test names are provided by the user. The tests are enqueued and submitted to the test execution engine. 12 | * The test execution results are fetched and results are published as JUnit xml report 13 | * Code coverage: Source class names are provided by the user. 14 | * The tool uses Tooling APIs to compute code coverage for the source class(es) 15 | * A consolidated report is generated with covered/uncovered line numbers in the code. 16 | * User can also provide regex(es) to select the source and test class names 17 | * Thresholds for code coverage can be customized(default: 75%) by the user. 18 | * The tool errors out if code coverage threshold metrics are not met 19 | * 20 | * @author adarsh.ramakrishna@salesforce.com 21 | */ 22 | 23 | package com.sforce.cd.apexUnit; 24 | 25 | import java.util.Arrays; 26 | import java.util.List; 27 | 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import com.beust.jcommander.JCommander; 32 | import com.beust.jcommander.ParameterDescription; 33 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 34 | import com.sforce.cd.apexUnit.client.codeCoverage.CodeCoverageComputer; 35 | import com.sforce.cd.apexUnit.client.testEngine.TestExecutor; 36 | import com.sforce.cd.apexUnit.client.testEngine.TestStatusPollerAndResultHandler; 37 | import com.sforce.cd.apexUnit.report.ApexClassCodeCoverageBean; 38 | import com.sforce.cd.apexUnit.report.ApexCodeCoverageReportGenerator; 39 | import com.sforce.cd.apexUnit.report.ApexReportBean; 40 | import com.sforce.cd.apexUnit.report.ApexUnitTestReportGenerator; 41 | import com.sforce.cd.apexUnit.report.ApexUnitCodeCoverageResults; 42 | 43 | public class ApexUnitRunner { 44 | private static Logger LOG = LoggerFactory.getLogger(ApexUnitRunner.class); 45 | 46 | /* 47 | * main method: entry point to the ApexUnit 2.x 48 | * 49 | * @param: arguments as a String array 50 | */ 51 | public static void main(String[] args) { 52 | Long start = System.currentTimeMillis(); 53 | // Read input arguments using JCommander 54 | CommandLineArguments cmdLineArgs = new CommandLineArguments(); 55 | JCommander jcommander = new JCommander(cmdLineArgs, args); 56 | boolean skipCodeCoverageComputation = CommandLineArguments.getTeamCodeCoverageThreshold() == 0 57 | && CommandLineArguments.getOrgWideCodeCoverageThreshold() == 0; 58 | if (CommandLineArguments.isHelp()) { 59 | logHelp(jcommander); 60 | } 61 | if ((CommandLineArguments.getTestManifestFiles() == null && CommandLineArguments.getTestRegex() == null)) { 62 | ApexUnitUtils.shutDownWithErrMsg("Either of the test manifest file or test regex should be provided"); 63 | } 64 | if (CommandLineArguments.getClassManifestFiles() == null && CommandLineArguments.getSourceRegex() == null 65 | && !skipCodeCoverageComputation) { 66 | ApexUnitUtils.shutDownWithErrMsg("Either of the source class manifest file or source class regex should be provided"); 67 | } 68 | // Invoke the FlowController.logicalFlow() that handles the entire 69 | // logical flow of ApexUnit tool. 70 | TestExecutor testExecutor = new TestExecutor(); 71 | LOG.info("#################################### " 72 | + "Processing the Apex test classes specified by the user" 73 | + " #################################### "); 74 | ApexReportBean[] apexReportBeans = testExecutor.testExecutionFlow(); 75 | 76 | // constructing report file with test results and code coverage results 77 | String runTimeExceptionMessage = ""; 78 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = null; 79 | 80 | // skip code coverage computation if team code coverage and org wide 81 | // code coverage thresholds are set to 0 82 | if (!skipCodeCoverageComputation) { 83 | LOG.info("#################################### " 84 | + "Computing code coverage for the team based on the Apex Class names(source class names) provided" 85 | + " #################################### "); 86 | 87 | CodeCoverageComputer toolingAPIInvoker = new CodeCoverageComputer(); 88 | apexClassCodeCoverageBeans = toolingAPIInvoker.calculateAggregatedCodeCoverageUsingToolingAPI(); 89 | // sort the codeCoverageBeans before generating report so that 90 | // records 91 | // are ordered in ascending order of code coverage percentage 92 | Arrays.sort(apexClassCodeCoverageBeans); 93 | // computes org wide code coverage 94 | toolingAPIInvoker.getOrgWideCodeCoverage(); 95 | // generate the reports for publishing 96 | } else { 97 | LOG.info("#################################### Skipping code coverage computation. " 98 | + "Please update the threshold parameters for team code coverage and " 99 | + "org wide code coverage to activate the code coverage computation feature" 100 | + " #################################### "); 101 | } 102 | Long end = System.currentTimeMillis(); 103 | LOG.debug("Total Time taken by ApexUnit tool in secs: " + (end - start) / 1000); 104 | if (apexReportBeans != null && apexReportBeans.length > 0) { 105 | LOG.info("Total test methods executed: " + apexReportBeans.length); 106 | String reportFile = "ApexUnitReport.xml"; 107 | ApexUnitTestReportGenerator.generateTestReport(apexReportBeans, reportFile); 108 | } else { 109 | ApexUnitUtils.shutDownWithErrMsg("Unable to generate test report. " 110 | + "Did not find any test results for the job id"); 111 | } 112 | if (!skipCodeCoverageComputation) { 113 | ApexCodeCoverageReportGenerator.generateHTMLReport(apexClassCodeCoverageBeans); 114 | 115 | // validating the code coverage metrics against the thresholds 116 | // provided by the user 117 | boolean teamCodeCoverageThresholdError = false; 118 | boolean orgWideCodeCoverageThresholdError = false; 119 | if (ApexUnitCodeCoverageResults.teamCodeCoverage < CommandLineArguments.getTeamCodeCoverageThreshold()) { 120 | if (ApexUnitCodeCoverageResults.teamCodeCoverage == -1) { 121 | LOG.warn("No source class names provided. Team Code coverage not computed "); 122 | } else { 123 | teamCodeCoverageThresholdError = true; 124 | } 125 | } 126 | if (ApexUnitCodeCoverageResults.orgWideCodeCoverage < CommandLineArguments 127 | .getOrgWideCodeCoverageThreshold()) { 128 | orgWideCodeCoverageThresholdError = true; 129 | } 130 | 131 | if (teamCodeCoverageThresholdError) { 132 | runTimeExceptionMessage += "Failed to meet the Team code coverage threshold : " 133 | + CommandLineArguments.getTeamCodeCoverageThreshold() 134 | + " The team code coverage for the given classes is: " 135 | + ApexUnitCodeCoverageResults.teamCodeCoverage + "%\n" 136 | + "Calibrate your threshold values if you are happy with the current code coverage\n"; 137 | } 138 | if (orgWideCodeCoverageThresholdError) { 139 | runTimeExceptionMessage += "Failed to meet the Org code coverage threshold : " 140 | + CommandLineArguments.getOrgWideCodeCoverageThreshold() 141 | + " The org code coverage for the org is: " + ApexUnitCodeCoverageResults.orgWideCodeCoverage 142 | + "%" + "Calibrate your threshold values if you are happy with the current code coverage\n"; 143 | } 144 | } 145 | 146 | // if there are test failures, concatenate error messages 147 | if (TestStatusPollerAndResultHandler.testFailures) { 148 | runTimeExceptionMessage += "Test failures amongst the Apex tests executed. "; 149 | if (TestStatusPollerAndResultHandler.failedTestMethods != null 150 | && TestStatusPollerAndResultHandler.failedTestMethods.size() > 0) { 151 | int failedTestsCount = TestStatusPollerAndResultHandler.failedTestMethods.size(); 152 | runTimeExceptionMessage += "Failed test methods count: " + failedTestsCount + " Failed test methods: " 153 | + TestStatusPollerAndResultHandler.failedTestMethods.toString(); 154 | } 155 | } 156 | // if there are any runtime exceptions, throw the error messages and 157 | // shut down ApexUnit 158 | if (!runTimeExceptionMessage.equals("")) { 159 | ApexUnitUtils.shutDownWithErrMsg(runTimeExceptionMessage); 160 | } else { 161 | LOG.info("Success!! No test failures and all code coverage thresholds are met!! Exiting ApexUnit.. Good bye.."); 162 | } 163 | 164 | } 165 | 166 | /* 167 | * Log command line options for -help option 168 | * @param jcommander - JCommander instance 169 | * @return nothing. Display the description for each CLI parameter 170 | */ 171 | private static void logHelp(JCommander jcommander) { 172 | List parameters = jcommander.getParameters(); 173 | for (ParameterDescription parameter : parameters) { 174 | LOG.info(parameter.getLongestName() + " : " + parameter.getDescription()); 175 | } 176 | System.exit(0); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/forcedotcom/ApexUnit.svg?branch=master)](https://travis-ci.org/forcedotcom/ApexUnit) 2 | ApexUnit 3 | ======== 4 | 5 | ## What is ApexUnit? 6 | ApexUnit is a continuous integration tool for the Force.com platform that executes Apex tests and fetches code coverage results. 7 | 8 | ApexUnit comprises of two major components: 9 | - An xUnit based testing framework for the Force.com platform 10 | - Extensive code coverage metrics with actionable detail for specified Apex classes 11 | 12 | ## Key Features of ApexUnit 13 | Please refer https://github.com/forcedotcom/ApexUnit/wiki to learn more about the key features of ApexUnit and its usage 14 | 15 | ## Pre-Requisites 16 | - Java 1.6 or later 17 | - http://www.oracle.com/technetwork/java/javase/downloads/index.html 18 | - Maven 3.0.3 or later (latest version is recommended) 19 | - Download link : https://maven.apache.org/download.cgi 20 | - Installation instructions : https://maven.apache.org/install.html 21 | - Configuring maven : https://maven.apache.org/configure.html 22 | - OAuth setup for the org to retrieve the Client ID and Client Secret using a Connected App 23 | - http://salesforce.stackexchange.com/questions/40346/where-do-i-find-the-client-id-and-client-secret-of-an-existing-connected-app 24 | - http://stackoverflow.com/questions/18464598/where-to-get-client-id-and-client-secret-of-salesforce-api-for-rails-3-2-11 25 | - http://www.calvinfroedge.com/salesforce-how-to-generate-api-credentials/ 26 | - Please verify the oauth setup for the org by executing the following command: 27 | ```shell 28 | curl -v /services/oauth2/token -d "grant_type=password" -d "client_id=*CLIENT_ID_GOES_HERE*" -d "client_secret= *CLIENT_SECRET_GOES_HERE*" -d "username=*yourusername@yourdomain.com*" -d "password= *your_password_goes_here+*" 29 | ``` 30 | *The above command should provide you the access_token in a JSON formatted response. If you are running this command from an IP address that is outside of the trusted IP range specified by your connected app, you must append a [security token](https://help.salesforce.com/apex/HTViewHelpDoc?id=user_security_token.htm&language=en) to your password. 31 | + If your password contains special characters, you must pass in a URL encoded version of your password. Note that this only needs to be done for the curl command and not the maven command 32 | 33 | ## How to build and execute ApexUnit 34 | - Clone the project onto your local system using the command: 35 | ```shell 36 | git clone https://github.com/forcedotcom/ApexUnit.git 37 | ``` 38 | This would create a local copy of the project for you. 39 | - (Optional) Open the project in an IDE (Eclipse, IntelliJ, etc.) 40 | - There are two ways you can select test classes to execute and select classes you wish to examine the code coverage of 41 | - regex - identify and provide regex for the test class names that you want to execute in the "-regex.for.selecting.test.classes.to.execute" parameter. Example: if you want to execute the tests: My_Apex_controller_Test, My_Apex_builder_Test and My_Apex_validator_Test, identify the regex as "My_Apex_\*_Test". Pass the parameter "-regex.for.selecting.test.classes.to.execute My_Apex_\*_Test" in the mvn command. 42 | - Similarly, you can provide regex for the classes for which you want to examine the code coverage of by using "-regex.for.selecting.source.classes.for.code.coverage.computation My_Apex_\*_Class" in the mvn command. 43 | - Manifest files - Lists of tests can be read from Manifest files. Create a manifest file such as ManifestFile_Unit_Tests.txt in the "src/main/resources" directory of your project. Add test class names to execute in the manifest file. Specify this manifest file in the mvn command like "-manifest.files.with.test.class.names.to.execute ManifestFile_Unit_Tests.txt". 44 | - Similarly, add the class names for which you want to exercise code coverage in a manifest file such as ClasssManifestFile.txt in "src/main/resources" directory of your project and specify this manifest file in the mvn command like "-manifest.files.with.source.class.names.for.code.coverage.computation ClassManifestFile.txt". 45 | - Note that multiple regexes and manifest files can be specified using comma seperation(without spaces). Example: "-regex.for.selecting.test.classes.to.execute This_Is_Regex1\*,\*Few_Test,Another_\*_regex -manifest.files.with.source.class.names.for.code.coverage.computation ClassManifestFile.txt,MoreClassesManifestFile.txt" 46 | - Go to your project directory (the directory containing pom.xml) in your commandline and execute the following command: 47 | ```java 48 | mvn compile exec:java -Dexec.mainClass="com.sforce.cd.apexUnit.ApexUnitRunner" 49 | -Dexec.args="-org.login.url $Salesforce_org_url 50 | -org.username $username -org.password $password 51 | -org.client.id $client_id 52 | -org.client.secret $client_secret 53 | -org.wide.code.coverage.threshold $Org_Wide_Code_Coverage_Percentage_Threshold 54 | -team.code.coverage.threshold $team_Code_Coverage_Percentage_Threshold 55 | -regex.for.selecting.source.classes.for.code.coverage.computation 56 | $regex_For_Apex_Classes_To_Compute_Code_Coverage 57 | -regex.for.selecting.test.classes.to.execute $regex_For_Apex_Test_Classes_To_Execute 58 | -manifest.files.with.test.class.names.to.execute 59 | $manifest_Files_For_Apex_Test_Classes_To_Execute 60 | -manifest.files.with.source.class.names.for.code.coverage.computation 61 | $manifest_Files_For_Apex_Source_Classes_to_compute_code_coverage 62 | -max.test.execution.time.threshold 63 | $max_time_threshold_for_test_execution_to_abort" 64 | -proxy.host 65 | $prox_host 66 | -proxy.port 67 | $proxy_port 68 | 69 | ``` 70 | *Please replace all $xyz with the values specific to your environment/project* 71 | 72 | Required parameters: 73 | - -org.login.url : Login URL for the org (for example, https://na14.salesforce.com) 74 | - -org.username : Username for the org 75 | - -org.password : Password corresponding to the username for the org 76 | - -org.client.id : Client ID associated with the org. 77 | - -org.client.secret : Client Secret associated with the org. 78 | 79 | Optional Parameters: 80 | - -org.wide.code.coverage.threshold (default value: 75) : Org wide minimum code coverage required to meet the code coverage standards 81 | - -team.code.coverage.threshold (default value: 75) : Team wide minimum code coverage required to meet the code coverage standards 82 | - -regex.for.selecting.source.classes.for.code.coverage.computation : The source regex used by the team for the apex source classes. All classes beginning with this parameter in the org will be used to compute team code coverage 83 | - -regex.for.selecting.test.classes.to.execute : The test regex used by the team for the apex test classes. All tests beginning with this parameter in the org will be selected to run 84 | - -manifest.files.with.test.class.names.to.execute : Manifest files containing the list of test classes to be executed 85 | - -manifest.files.with.source.class.names.for.code.coverage.computation : Manifest files containing the list of Apex classes for which code coverage is to be computed 86 | - -max.test.execution.time.threshold : Maximum execution time(in minutes) for a test before it gets aborted 87 | - -proxy.host : Proxy host for external access 88 | - -proxy.port : Proxy port for external access 89 | - -help : Displays options available for running this application 90 | 91 | Note: You must provide either of the (-regex.for.selecting.source.classes.for.code.coverage.computation OR -manifest.files.with.source.class.names.for.code.coverage.computation) AND either of -(regex.for.selecting.test.classes.to.execute OR -manifest.files.with.test.class.names.to.execute) 92 | 93 | Sample command: 94 | ```java 95 | mvn compile exec:java -Dexec.mainClass="com.sforce.cd.apexUnit.ApexUnitRunner" -Dexec.args=" -org.login.url https://na14.salesforce.com -org.username yourusername@salesforce.com -org.password yourpassword-org.wide.code.coverage.threshold 75 -team.code.coverage.threshold 80 -org.client.id CLIENT_ID_FROM_CONNECTED_APP -org.client.secret CLIENT_SECRET_FROM_CONNECTED_APP -regex.for.selecting.test.classes.to.execute your_regular_exp1_for_test_classes,your_regular_exp2_for_test_classes -regex.for.selecting.source.classes.for.code.coverage.computation your_regular_exp1_for_source_classes,your_regular_exp2_for_source_classes -manifest.files.with.test.class.names.to.execute ManifestFile.txt -manifest.files.with.source.class.names.for.code.coverage.computation ClassManifestFile.txt -max.test.execution.time.threshold 10 -proxy.host your.proxy-if-required.net -proxy.port 8080" 96 | ``` 97 | Note: Multiple comma separated manifest files and regexes can be provided. Please do not include spaces while providing multiple regex or manifest files. 98 | 99 | On successful completion - the command should indicate that your build succeeded and should generate two report files - **ApexUnitReport.xml** (This is the test report in JUnit format) and **Report/ApexUnitReport.html** (This is the code coverage report in html format) 100 | 101 | # Using Manifest files and Regexes 102 | 103 | You can populate class names in the Manifest file and/or provide regular expressions(regexes) 104 | Please refer https://github.com/forcedotcom/ApexUnit/wiki/Manifest-file-vs-regex for the usecases where manifest file(s) and regex(es) option can be used 105 | 106 | #Addional options 107 | 108 | Use src/main/resources/config.properties to set the below parameters. 109 | 110 | 1. API_VERSION(Default value: 37.0) : The Partner API version in use for the org. 111 | 112 | 2. MAX_TIME_OUT_IN_MS(Default value : 1200000 ==> 20 minutes) : Time out setting for the session, Once timeout occurs, session renewer module is invoked which renews the session. Helpful when you face a connection exception during query executions. 113 | 114 | ## Integrating with CI pipeline 115 | CI engines like Jenkins(https://jenkins-ci.org/) can be used to seamlessly integrate ApexUnit with CI pipelines. 116 | Please find the details here: https://github.com/forcedotcom/ApexUnit/wiki/Integrating-with-CI-pipeline 117 | 118 | ## How to contribute or track Bug/Issue for ApexUnit? 119 | https://github.com/forcedotcom/ApexUnit/wiki/Contribution-and-Bug-Issue-tracking 120 | 121 | ## Questions/Feedback? 122 | https://github.com/forcedotcom/ApexUnit/wiki/Contact-info -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/codeCoverage/WebServiceInvoker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to invoke web services calls: get and post methods for the REST APIs using OAUTH 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | package com.sforce.cd.apexUnit.client.codeCoverage; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.InputStreamReader; 19 | import java.io.UnsupportedEncodingException; 20 | import java.net.URLEncoder; 21 | import java.util.Collection; 22 | import java.util.HashMap; 23 | import java.util.HashSet; 24 | import java.util.Iterator; 25 | import java.util.Set; 26 | 27 | import org.apache.commons.httpclient.HttpClient; 28 | import org.apache.commons.httpclient.HostConfiguration; 29 | import org.apache.commons.httpclient.HttpException; 30 | import org.apache.commons.httpclient.URI; 31 | import org.apache.commons.httpclient.URIException; 32 | import org.apache.commons.httpclient.methods.GetMethod; 33 | import org.apache.commons.httpclient.methods.PostMethod; 34 | import org.apache.commons.httpclient.methods.StringRequestEntity; 35 | import org.json.simple.JSONObject; 36 | import org.json.simple.JSONValue; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | import com.google.gson.Gson; 41 | import com.google.gson.reflect.TypeToken; 42 | import com.sforce.cd.apexUnit.ApexUnitUtils; 43 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 44 | 45 | import static java.net.URLEncoder.encode; 46 | 47 | /* 48 | * WebServiceInvoker provides interfaces for get and post methods for the REST APIs using OAUTH 49 | */ 50 | public class WebServiceInvoker { 51 | private static Logger LOG = LoggerFactory.getLogger(WebServiceInvoker.class); 52 | 53 | /* 54 | * Utility to perform HTTP post operation on the orgUrl with the specific 55 | * sub-url as mentioned in the relativeServiceURL 56 | * 57 | * @param relativeServiceURL - relative service url w.r.t org url for firing 58 | * post request 59 | * 60 | * @return : hashmap with key-value pairs of response from the post query 61 | */ 62 | public HashMap doPost(String relativeServiceURL) { 63 | 64 | PostMethod post = null; 65 | HttpClient httpclient = new HttpClient(); 66 | String requestString = ""; 67 | HashMap responseMap = new HashMap(); 68 | 69 | 70 | try { 71 | // the client id and secret is applicable across all dev orgs 72 | requestString = generateRequestString(); 73 | String authorizationServerURL = CommandLineArguments.getOrgUrl() + relativeServiceURL; 74 | httpclient.getParams().setSoTimeout(0); 75 | 76 | // Set proxy if needed 77 | if (CommandLineArguments.getProxyHost() != null && CommandLineArguments.getProxyPort() != null) { 78 | LOG.debug("Setting proxy configuraiton to " + CommandLineArguments.getProxyHost() + " on port " 79 | + CommandLineArguments.getProxyPort()); 80 | HostConfiguration hostConfiguration = httpclient.getHostConfiguration(); 81 | hostConfiguration.setProxy(CommandLineArguments.getProxyHost(),CommandLineArguments.getProxyPort()); 82 | httpclient.setHostConfiguration(hostConfiguration); 83 | } 84 | 85 | post = new PostMethod(authorizationServerURL); 86 | post.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 87 | post.addRequestHeader("X-PrettyPrint", "1"); 88 | post.setRequestEntity(new StringRequestEntity(requestString, "application/x-www-form-urlencoded", "UTF-8")); 89 | httpclient.executeMethod(post); 90 | 91 | Gson json = new Gson(); 92 | // obtain the result map from the response body and get the access 93 | // token 94 | responseMap = json.fromJson(post.getResponseBodyAsString(), new TypeToken>() { 95 | }.getType()); 96 | 97 | } catch (Exception ex) { 98 | ApexUnitUtils.shutDownWithDebugLog(ex, "Exception during post method: " + ex); 99 | if(LOG.isDebugEnabled()) { 100 | ex.printStackTrace(); 101 | } 102 | } finally { 103 | post.releaseConnection(); 104 | } 105 | 106 | return responseMap; 107 | 108 | } 109 | 110 | public String generateRequestString() { 111 | String requestString = ""; 112 | try { 113 | requestString = "grant_type=password&client_id=" + CommandLineArguments.getClientId() + "&client_secret=" 114 | + CommandLineArguments.getClientSecret() + "&username=" + CommandLineArguments.getUsername() 115 | + "&password=" + encode(CommandLineArguments.getPassword(), "UTF-8"); 116 | } catch (UnsupportedEncodingException ex) { 117 | ApexUnitUtils.shutDownWithDebugLog(ex, "Exception during request string generation: " + ex); 118 | if(LOG.isDebugEnabled()) { 119 | ex.printStackTrace(); 120 | } 121 | } 122 | 123 | return requestString; 124 | } 125 | 126 | public static JSONObject doGet(String relativeServiceURL, String soql, String accessToken) { 127 | if (soql != null && !soql.equals("")) { 128 | try { 129 | relativeServiceURL += "/query/?q=" + encode(soql, "UTF-8"); 130 | } catch (UnsupportedEncodingException e) { 131 | 132 | ApexUnitUtils 133 | .shutDownWithDebugLog(e, "Error encountered while trying to encode the query string using UTF-8 format. The error says: "+ e.getMessage()); 134 | } 135 | } 136 | return doGet(relativeServiceURL, accessToken); 137 | } 138 | 139 | /* 140 | * method to perform get operation using the access token for the org and 141 | * return the json response 142 | * 143 | * @param relativeServiceURL - relative service url w.r.t org url for firing 144 | * post request 145 | * 146 | * @param accessToken : access token for the org(generated in the post 147 | * method) 148 | * 149 | * @return : json response from the get request 150 | */ 151 | public static JSONObject doGet(String relativeServiceURL, String accessToken) { 152 | 153 | LOG.debug("relativeServiceURL in doGet method:" + relativeServiceURL); 154 | HttpClient httpclient = new HttpClient(); 155 | // Set proxy if needed 156 | if (CommandLineArguments.getProxyHost() != null && CommandLineArguments.getProxyPort() != null) { 157 | LOG.debug("Setting proxy configuraiton to " + CommandLineArguments.getProxyHost() + " on port " 158 | + CommandLineArguments.getProxyPort()); 159 | HostConfiguration hostConfiguration = httpclient.getHostConfiguration(); 160 | hostConfiguration.setProxy(CommandLineArguments.getProxyHost(),CommandLineArguments.getProxyPort()); 161 | httpclient.setHostConfiguration(hostConfiguration); 162 | } 163 | GetMethod get = null; 164 | 165 | String authorizationServerURL = CommandLineArguments.getOrgUrl() + relativeServiceURL; 166 | get = new GetMethod(authorizationServerURL); 167 | get.addRequestHeader("Content-Type", "application/json"); 168 | get.setRequestHeader("Authorization", "Bearer " + accessToken); 169 | LOG.debug("Start GET operation for the url..." + authorizationServerURL); 170 | InputStream instream = null; 171 | try { 172 | instream = executeHTTPMethod(httpclient, get, authorizationServerURL); 173 | LOG.debug("done with get operation"); 174 | 175 | JSONObject json = (JSONObject) JSONValue.parse(new InputStreamReader(instream)); 176 | LOG.debug("is json null? :" + json == null ? "true" : "false"); 177 | if (json != null) { 178 | if (LOG.isDebugEnabled()) { 179 | LOG.debug("ToolingApi.get response: " + json.toString()); 180 | Set keys = castSet(String.class, json.keySet()); 181 | Iterator jsonKeyIter = keys.iterator(); 182 | LOG.debug("Response for the GET method: "); 183 | while (jsonKeyIter.hasNext()) { 184 | String key = jsonKeyIter.next(); 185 | LOG.debug("key : " + key + ". Value : " + json.get(key) + "\n"); 186 | // TODO if query results are too large, only 1st batch 187 | // of results 188 | // are returned. Need to use the identifier in an 189 | // additional query 190 | // to retrieve rest of the next batch of results 191 | 192 | if (key.equals("nextRecordsUrl")) { 193 | // fire query to the value for this key 194 | // doGet((String) json.get(key), accessToken); 195 | try { 196 | authorizationServerURL = CommandLineArguments.getOrgUrl() + (String) json.get(key); 197 | get.setURI(new URI(authorizationServerURL, false)); 198 | instream = executeHTTPMethod(httpclient, get, authorizationServerURL); 199 | JSONObject newJson = (JSONObject) JSONValue.parse(new InputStreamReader(instream)); 200 | if (newJson != null) { 201 | Set newKeys = castSet(String.class, json.keySet()); 202 | Iterator newJsonKeyIter = newKeys.iterator(); 203 | while (newJsonKeyIter.hasNext()) { 204 | String newKey = newJsonKeyIter.next(); 205 | json.put(newKey, newJson.get(newKey)); 206 | LOG.debug("newkey : " + newKey + ". NewValue : " + newJson.get(newKey) + "\n"); 207 | } 208 | } 209 | 210 | } catch (URIException e) { 211 | ApexUnitUtils.shutDownWithDebugLog(e, "URI exception while fetching subsequent batch of result"); 212 | } 213 | } 214 | 215 | } 216 | } 217 | } 218 | return json; 219 | } finally { 220 | get.releaseConnection(); 221 | try { 222 | if (instream != null) { 223 | instream.close(); 224 | 225 | } 226 | } catch (IOException e) { 227 | ApexUnitUtils 228 | .shutDownWithDebugLog(e, "Encountered IO exception when closing the stream after reading response from the get method. The error says: "+ e.getMessage()); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * 235 | * execute the HTTP Get method and return response as Input stream 236 | * 237 | * @param httpclient 238 | * HTTPClient 239 | * @param get 240 | * GetMethod 241 | * @return 242 | * @throws IOException 243 | * @throws HttpException 244 | */ 245 | 246 | private static InputStream executeHTTPMethod(HttpClient httpclient, GetMethod get, 247 | String authorizationServerURL) { 248 | try { 249 | httpclient.executeMethod(get); 250 | } catch (HttpException e) { 251 | ApexUnitUtils 252 | .shutDownWithDebugLog(e, "Encountered HTTP exception when executing get method using OAuth authentication for the url "+ authorizationServerURL 253 | +". The error says: "+ e.getMessage()); 254 | } catch (IOException e) { 255 | ApexUnitUtils 256 | .shutDownWithDebugLog(e, "Encountered IO exception when executing get method using OAuth authentication for the url "+ authorizationServerURL 257 | +". The error says: "+ e.getMessage()); 258 | } 259 | LOG.info("Status code : " 260 | + get.getStatusCode() + " Status message from the get request:" + get.getStatusText() + " Reason phrase: "+get.getStatusLine().getReasonPhrase()); 261 | 262 | InputStream instream = null; 263 | try { 264 | // don't delete the below line --i.e. getting response body as 265 | // string. Getting response as stream fails upon deleting the below 266 | // line! strange! 267 | String respStr; 268 | respStr = get.getResponseBodyAsString(); 269 | instream = get.getResponseBodyAsStream(); 270 | } catch (IOException e) { 271 | 272 | ApexUnitUtils 273 | .shutDownWithDebugLog(e, "Encountered IO exception when obtaining response body for the get method. The error says: "+ e.getMessage()); 274 | } 275 | return instream; 276 | } 277 | 278 | /* 279 | * Method to cast a collection of objects to a set of given type 280 | * 281 | * @param clazz - class type of the collection 282 | * 283 | * @param c - collection set 284 | * 285 | * @return set - A set returned that is of the class 'clazz' 286 | */ 287 | public static Set castSet(Class clazz, Collection c) { 288 | Set set = new HashSet(); 289 | for (Object o : c) 290 | set.add(clazz.cast(o)); 291 | return set; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/QueryConstructor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * QueryConstructor class for constructing queries that are fired by the ApexUnit tool 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.client; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import com.sforce.soap.partner.sobject.SObject; 21 | 22 | /* 23 | * Constructs and returns queries for the client to invoke web services and fetch the data from the org 24 | */ 25 | public class QueryConstructor { 26 | private static Logger logger = LoggerFactory.getLogger(QueryConstructor.class); 27 | /* 28 | * construct query that fetches Id and Name of the ApexClass based on regex 29 | * provided by the user * is converted to % in the regex. If no * is found 30 | * in the regex, the regex is considered as a prefix and a % is added at the 31 | * end of regex 32 | * 33 | * @param regex - regex for ApexClass as String 34 | * 35 | * @return - Query to fetch apex classes as String 36 | */ 37 | public static String generateQueryToFetchApexClassesBasedOnRegex(String namespace, String regex) { 38 | String processedRegex = processRegexForSoqlQueries(regex); 39 | String soql = "SELECT Id , Name FROM ApexClass WHERE NamespacePrefix ="+ escapeSingleQuote(namespace) +" AND " 40 | + "Name like " + escapeSingleQuote(processedRegex); 41 | return soql; 42 | } 43 | 44 | /* 45 | * construct query that fetches Id and Name of the ApexTrigger based on 46 | * regex provided by the user * is converted to % in the regex. If no * is 47 | * found in the regex, the regex is considered as a prefix and a % is added 48 | * at the end of regex 49 | * 50 | * @param regex - regex for ApexTrigger as String 51 | * 52 | * @return - Query to fetch apex triggers as String 53 | */ 54 | public static String generateQueryToFetchApexTriggersBasedOnRegex(String namespace, String regex) { 55 | String processedRegex = processRegexForSoqlQueries(regex); 56 | String soql = "SELECT Id , Name FROM ApexTrigger WHERE NamespacePrefix ="+ escapeSingleQuote(namespace) +" AND " 57 | + "Name like " + escapeSingleQuote(processedRegex); 58 | return soql; 59 | } 60 | 61 | /* 62 | * construct query that fetches Id and Name of the ApexClass for a given 63 | * ApexClassId 64 | * 65 | * @param apexClassId - apexClassId as String 66 | * 67 | * @return - Query to fetch apex class as String 68 | */ 69 | public static String generateQueryToFetchApexClassFromId(String apexClassId) { 70 | String soql = "SELECT Id, Name FROM ApexClass WHERE Id = " + escapeSingleQuote(apexClassId); 71 | return soql; 72 | } 73 | 74 | /* 75 | * construct query that fetches Id and Name of the ApexTrigger for a given 76 | * ApexClassId 77 | * 78 | * @param apexTriggerId - ApexTrigger Id as String 79 | * 80 | * @return - Query to fetch apex trigger as String 81 | */ 82 | public static String generateQueryToFetchApexTriggerFromId(String apexTriggerId) { 83 | String soql = ""; 84 | if (apexTriggerId != null && !apexTriggerId.equals("")) { 85 | soql = "SELECT Id, Name FROM ApexTrigger WHERE Id = " + escapeSingleQuote(apexTriggerId); 86 | } 87 | return soql; 88 | } 89 | 90 | /* 91 | * construct query that fetches Id and Name of the ApexClass for a given 92 | * ApexClassName 93 | * 94 | * @param apexClassName - Apex class name as String 95 | * 96 | * @return - Query to fetch apex class as String 97 | */ 98 | public static String generateQueryToFetchApexClass(String namespace, String apexClassName) { 99 | String soql = ""; 100 | if (apexClassName != null && !apexClassName.equals("")) { 101 | soql = "SELECT Id, Name FROM ApexClass WHERE NamespacePrefix ="+ escapeSingleQuote(namespace) +" AND " 102 | + "Name = " + escapeSingleQuote(apexClassName); 103 | } 104 | return soql; 105 | } 106 | 107 | /* 108 | * construct query that fetches Id and Name of the ApexTrigger for a given 109 | * ApexTriggerName 110 | * 111 | * @param apexClassName - Apex trigger name as String 112 | * 113 | * @return - Query to fetch apex trigger as String 114 | */ 115 | public static String generateQueryToFetchApexTrigger(String namespace, String apexTriggerName) { 116 | String soql = ""; 117 | if (apexTriggerName != null && !apexTriggerName.equals("")) { 118 | soql = "SELECT Id, Name FROM ApexTrigger WHERE NamespacePrefix ="+ escapeSingleQuote(namespace) +" AND " 119 | + "Name = " + escapeSingleQuote(apexTriggerName); 120 | } 121 | return soql; 122 | } 123 | 124 | /* 125 | * construct query that fetches parent job Id for a given ApexTestQueueItem 126 | * Id 127 | * 128 | * @param apexTestQueueItemId - Apex trigger name as String 129 | * 130 | * @return - Query to fetch apex trigger as String 131 | */ 132 | public static String fetchParentJobIdForApexTestQueueItem(String testQueueItemId) { 133 | String soql = ""; 134 | if (testQueueItemId != null && !testQueueItemId.equals("")) { 135 | // we need to limit the number of records we fetch to 1 because 136 | // multiple records are returned for each ApexTestClass 137 | soql = "select ParentJobId from ApexTestQueueItem where id = " + escapeSingleQuote(testQueueItemId) 138 | + " limit 1"; 139 | } 140 | return soql; 141 | } 142 | 143 | /* 144 | * construct query that fetches the test execution status and related info 145 | * from ApexTestQueueItem table for a given parentJob Id 146 | * 147 | * @param parentJobId - Parent job ID as String 148 | * 149 | * @return - Query to fetch test execution status as String 150 | */ 151 | public static String getTestExecutionStatus(String parentJobId) { 152 | String soql = ""; 153 | if (parentJobId != null && !parentJobId.equals("")) { 154 | soql = "Select Id, ApexClassId, ApexClass.Name, ExtendedStatus, ParentJobId, Status, SystemModstamp, CreatedDate " 155 | + "From ApexTestQueueItem Where ParentJobId = "+ escapeSingleQuote(parentJobId); 156 | } 157 | return soql; 158 | } 159 | 160 | /* 161 | * construct query that fetches the test result and related info from 162 | * ApexTestResult table for a given parentJob Id 163 | * 164 | * @param parentJobId - Parent job ID as String 165 | * 166 | * @return - Query to fetch test result as String 167 | */ 168 | public static String fetchResultFromApexTestQueueItem(String parentJobId) { 169 | String soql = ""; 170 | if (parentJobId != null && !parentJobId.equals("")) { 171 | soql = "SELECT ApexClassId,AsyncApexJobId,Id,Message,MethodName,Outcome,QueueItemId,RunTime,StackTrace,SystemModstamp,TestTimestamp " 172 | + "FROM ApexTestResult WHERE AsyncApexJobId = "+ escapeSingleQuote(parentJobId); 173 | } 174 | return soql; 175 | } 176 | 177 | /* 178 | * construct query that fetches the count of the test result from 179 | * ApexTestQueueItem table for a given parentJob Id 180 | * 181 | * @param parentJobId - Parent job ID as String 182 | * 183 | * @return - Query to fetch count of the test result as String 184 | */ 185 | public static String fetchTestClassCountForParentJobId(String parentJobId) { 186 | String soql = ""; 187 | if (parentJobId != null && !parentJobId.equals("")) { 188 | soql = "select count() from ApexTestQueueItem where ParentJobId = '" + parentJobId + "'"; 189 | } 190 | return soql; 191 | } 192 | 193 | /* 194 | * construct query that computes aggregated code coverage metrics for a 195 | * given array of classes 196 | * 197 | * @param parentJobId - array of class names as String 198 | * 199 | * @return - Query to compute aggregated code coverage metrics as String 200 | */ 201 | public static String getAggregatedCodeCoverage(String classArrayForQuery) { 202 | String soql = ""; 203 | if (classArrayForQuery != null && !classArrayForQuery.equals("")) { 204 | soql = "select ApexClassorTriggerId,NumLinesCovered,NumLinesUncovered,Coverage FROM " 205 | + "ApexCodeCoverageAggregate WHERE ApexClassOrTriggerId IN (" + classArrayForQuery + ")"; 206 | } 207 | return soql; 208 | } 209 | 210 | /* 211 | * construct query that computes code coverage metrics for a given array of 212 | * classes at each method level (to be used later. not in use currently) 213 | * 214 | * @param parentJobId - array of class names as String 215 | * 216 | * @return - Query to compute code coverage metrics as String 217 | */ 218 | 219 | public static String getClassLevelCodeCoverage(String classArrayForQuery) { 220 | String soql = "select ApexClassOrTriggerId,NumLinesCovered,ApexTestClassId,NumLinesUncovered,TestMethodName FROM " 221 | + "ApexCodeCoverage WHERE ApexClassOrTriggerId IN (" + classArrayForQuery + ")"; 222 | return soql; 223 | } 224 | 225 | /* 226 | * construct query that computes org wide code coverage metrics for all the 227 | * classes in the prg 228 | * 229 | * @return - Query to compute org wide code coverage metrics as String 230 | */ 231 | public static String getOrgWideCoverage() { 232 | String soql = "select PercentCovered FROM ApexOrgWideCoverage"; 233 | return soql; 234 | } 235 | 236 | /* 237 | * construct query that fetches class info of the ApexClass for a given 238 | * ApexClassId 239 | * 240 | * @param apexClassId - Apex class id as String 241 | * 242 | * @return - Query to fetch info of apex class as String 243 | */ 244 | public static String getApexClassInfo(String apexClassId) { 245 | String soql = ""; 246 | if (apexClassId != null && !apexClassId.equals("")) { 247 | soql = "SELECT Id,Name,ApiVersion,LengthWithoutComments FROM ApexClass where Id = " 248 | + escapeSingleQuote(apexClassId); 249 | } 250 | return soql; 251 | } 252 | 253 | /* 254 | * construct query that fetches info of the ApexTrigger for a given Apex 255 | * class(trigger) Id 256 | * 257 | * @param apexClassId - Apex trigger id as String 258 | * 259 | * @return - Query to fetch info on apex trigger as String 260 | */ 261 | public static String getApexTriggerInfo(String apexClassId) { 262 | String soql = ""; 263 | if (apexClassId != null && !apexClassId.equals("")) { 264 | soql = "SELECT Id,Name,ApiVersion,LengthWithoutComments FROM ApexTrigger where Id = " 265 | + escapeSingleQuote(apexClassId); 266 | } 267 | return soql; 268 | } 269 | 270 | /* 271 | * Process regex provided by the user so that the regex can be consumed by 272 | * soql queries * is converted to % in the regex. If no * is found in the 273 | * regex, the regex is considered as a prefix and a % is added at the end of 274 | * regex 275 | * 276 | * @param apexClassNameRegex - regex for the apexclass name (provided by the 277 | * user) 278 | * 279 | * @return processed regex as a String 280 | */ 281 | private static String processRegexForSoqlQueries(String apexClassNameRegex) { 282 | if (apexClassNameRegex != null) { 283 | String processedRegexForSoqlQueries = apexClassNameRegex.replace('*', '%'); 284 | // if there are no '*'s in the given regex, convert it to prefix by 285 | // default 286 | if (processedRegexForSoqlQueries.equals(apexClassNameRegex)) { 287 | processedRegexForSoqlQueries += "%"; 288 | } 289 | return processedRegexForSoqlQueries; 290 | } 291 | return null; 292 | } 293 | 294 | /* 295 | * Escape single quotes in the user input during query execution 296 | * 297 | * @param : userInput: String 298 | * 299 | * @return : singleQuotrEscapedStr : String 300 | */ 301 | private static String escapeSingleQuote(String userInput) { 302 | String singleQuoteEscapedStr=null; 303 | if (userInput != null) { 304 | singleQuoteEscapedStr = userInput.replaceAll("'", "\'"); 305 | singleQuoteEscapedStr = "'"+singleQuoteEscapedStr+"'"; 306 | } 307 | return singleQuoteEscapedStr; 308 | } 309 | /* 310 | * Query to check class exists in Apex Test queue 311 | * @param : userInput: String 312 | * 313 | * @return : singleQuotrEscapedStr : String 314 | * 315 | */ 316 | 317 | public static String getQueryForApexClassInfo(String apexClassess){ 318 | String sql =""; 319 | if(apexClassess !=null){ 320 | sql = "SELECT Id FROM ApexTestQueueItem WHERE ApexClassId IN "+ "("+apexClassess+") AND status IN ('Queued', 'Processing')"; 321 | } 322 | return sql; 323 | } 324 | 325 | 326 | 327 | } 328 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/testEngine/TestStatusPollerAndResultHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to poll and fetch the results for the ApexUnit test executions 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.client.testEngine; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import org.apache.commons.lang.time.StopWatch; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import com.sforce.cd.apexUnit.ApexUnitUtils; 25 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 26 | import com.sforce.cd.apexUnit.client.QueryConstructor; 27 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 28 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 29 | import com.sforce.cd.apexUnit.report.ApexReportBean; 30 | import com.sforce.soap.partner.PartnerConnection; 31 | import com.sforce.soap.partner.QueryResult; 32 | import com.sforce.soap.partner.SaveResult; 33 | import com.sforce.soap.partner.sobject.SObject; 34 | import com.sforce.ws.ConnectionException; 35 | 36 | public class TestStatusPollerAndResultHandler { 37 | 38 | public static boolean testFailures = false; 39 | public static int totalTestMethodsExecuted = 0; 40 | public static int totalTestClasses = 0; 41 | public static int totalTestClassesAborted = 0; 42 | public static List failedTestMethods = new ArrayList(); 43 | 44 | private static Logger LOG = LoggerFactory.getLogger(TestStatusPollerAndResultHandler.class); 45 | 46 | public ApexReportBean[] fetchResultsFromParentJobId(String parentJobId, PartnerConnection conn) { 47 | waitForTestsToComplete(parentJobId, conn); 48 | LOG.info("All tests have now completed executing!!"); 49 | // Each test method execution is represented by a single ApexTestResult 50 | // record. 51 | // For example, if an Apex test class contains six test methods, 52 | // six ApexTestResult records are created. 53 | // These records are in addition to the ApexTestQueueItem record that 54 | // represents the Apex class. 55 | String soql = QueryConstructor.fetchResultFromApexTestQueueItem(parentJobId); 56 | 57 | LOG.debug(soql); 58 | ApexReportBean[] apexReportBeans = null; 59 | QueryResult queryResult = null; 60 | try { 61 | queryResult = conn.query(soql); 62 | } catch (ConnectionException e) { 63 | ApexUnitUtils.shutDownWithDebugLog(e, ConnectionHandler 64 | .logConnectionException(e, conn, soql)); 65 | } 66 | if (queryResult.getDone()) { 67 | int index = 0; 68 | SObject[] sObjects = queryResult.getRecords(); 69 | if (sObjects != null) { 70 | totalTestMethodsExecuted = sObjects.length; 71 | LOG.info("Total test methods executed: " + TestStatusPollerAndResultHandler.totalTestMethodsExecuted); 72 | apexReportBeans = new ApexReportBean[sObjects.length]; 73 | for (SObject sobject : sObjects) { 74 | ApexReportBean apexReportBean = populateReportBean(conn, sobject); 75 | apexReportBeans[index++] = apexReportBean; 76 | } 77 | } 78 | } 79 | return apexReportBeans; 80 | } 81 | 82 | private ApexReportBean populateReportBean(PartnerConnection conn, SObject sobject) { 83 | 84 | String apexClassId = sobject.getField("ApexClassId").toString(); 85 | ApexReportBean apexReportBean = null; 86 | if (apexClassId != null) { 87 | apexReportBean = new ApexReportBean(); 88 | apexReportBean.setApexClassId(sobject.getField("ApexClassId").toString()); 89 | if (ApexClassFetcherUtils.apexClassMap.get(apexReportBean.getApexClassId()) != null) { 90 | apexReportBean 91 | .setApexClassName(ApexClassFetcherUtils.apexClassMap.get(apexReportBean.getApexClassId())); 92 | } else { 93 | apexReportBean.setApexClassName( 94 | ApexClassFetcherUtils.fetchApexTestClassNameFromId(conn, apexReportBean.getApexClassId())); 95 | } 96 | if (sobject.getField("MethodName") != null) { 97 | apexReportBean.setMethodName(sobject.getField("MethodName").toString()); 98 | } 99 | if (sobject.getField("Message") != null) { 100 | apexReportBean.setMessage(sobject.getField("Message").toString()); 101 | } 102 | if (sobject.getField("Outcome") != null) { 103 | String outcome = sobject.getField("Outcome").toString(); 104 | apexReportBean.setOutcome(outcome); 105 | if (outcome.equalsIgnoreCase("fail") || outcome.equalsIgnoreCase("compilefail")) { 106 | testFailures = true; 107 | failedTestMethods.add(apexReportBean.getApexClassName() + "." + apexReportBean.getMethodName()); 108 | } 109 | } 110 | if (sobject.getField("RunTime") != null) { 111 | apexReportBean.setTimeElapsed(Long.parseLong(sobject.getField("RunTime").toString())); 112 | } 113 | if (sobject.getField("StackTrace") != null) { 114 | apexReportBean.setStackTrace(sobject.getField("StackTrace").toString()); 115 | } 116 | // LOG.info("SystemModstamp,TestTimestamp"+sobject.getField("SystemModstamp")+ 117 | // sobject.getField("TestTimestamp")); 118 | // LOG.info("ApexLog.DurationMilliseconds,ApexLog.Operation,ApexLog.Request,ApexLog.Status,ApexClass.Name"+ 119 | // sobject.getField("ApexLog.DurationMilliseconds")+","+sobject.getField("ApexLog.DurationMilliseconds") 120 | // +","+sobject.getField("ApexLog.Request")+","+sobject.getField("ApexLog.Status")+","+sobject.getField("ApexClass.Name")+"..."); 121 | } 122 | return apexReportBean; 123 | } 124 | 125 | public boolean waitForTestsToComplete(String parentJobId, PartnerConnection conn) { 126 | String soql = QueryConstructor.getTestExecutionStatus(parentJobId); 127 | // String soql = 128 | // QueryConstructor.getTestExecutionStatusAndTransactionTime(parentJobId); 129 | 130 | QueryResult queryResult; 131 | boolean testsCompleted = false; 132 | 133 | try { 134 | LOG.debug(soql); 135 | int index = 0; 136 | queryResult = conn.query(soql); 137 | 138 | if (queryResult.getDone()) { 139 | SObject[] sObjects = queryResult.getRecords(); 140 | 141 | if (sObjects != null) { 142 | String status = ""; 143 | int totalTests = sObjects.length; 144 | totalTestClasses = totalTests; 145 | int remainingTests = totalTests; 146 | LOG.info("Total test classes to execute: " + totalTestClasses); 147 | String testId = ""; 148 | String testName = ""; 149 | String id = ""; 150 | StopWatch stopWatch = new StopWatch(); 151 | long startTime = 0; 152 | long endTime = 0; 153 | for (SObject sobject : sObjects) { 154 | sobject.setType("ApexTestQueueItem"); 155 | status = sobject.getField("Status").toString(); 156 | testId = sobject.getField("ApexClassId").toString(); 157 | id = sobject.getField("Id").toString(); 158 | LOG.debug("ID for ApexTestQueueItem: " + id); 159 | testName = ApexClassFetcherUtils.apexClassMap.get(testId); 160 | LOG.info("Now executing the test class: " + testName + " (" + CommandLineArguments.getOrgUrl() 161 | + "/" + testId + " ) " + "Status : " + status); 162 | stopWatch.reset(); 163 | stopWatch.start(); 164 | startTime = stopWatch.getTime(); 165 | LOG.debug("Start time: " + startTime); 166 | 167 | while (status.equals("Processing") || status.equals("Queued") || status.equals("Preparing") 168 | || !status.equals("Completed")) { 169 | 170 | // break out of the loop if the test failed 171 | if (status.equals("Failed")) { 172 | LOG.info("Test class failure for : " + testName + " (" 173 | + CommandLineArguments.getOrgUrl() + "/" + testId + " ) "); 174 | break; 175 | } else if (status.equals("Aborted")) { 176 | LOG.info("Test : " + testName + " (" + CommandLineArguments.getOrgUrl() + "/" + testId 177 | + " ) has been aborted."); 178 | totalTestClassesAborted++; 179 | break; 180 | } 181 | // Abort the long running tests based on user 182 | // input(default: 10 minutes) 183 | // stopWatch.getTime() will be in milliseconds, 184 | // hence divide by 1000 to convert to seconds 185 | // maxTestExecTimeThreshold will be in minutes, 186 | // hence multiply by 60 to convert to seconds 187 | 188 | if (CommandLineArguments.getMaxTestExecTimeThreshold() != null && stopWatch.getTime() 189 | / 1000.0 > CommandLineArguments.getMaxTestExecTimeThreshold() * 60 190 | && status.equals("Processing")) { 191 | LOG.info("Oops! This test is a long running test. " 192 | + CommandLineArguments.getMaxTestExecTimeThreshold() 193 | + " minutes elapsed; aborting the test: " + testName); 194 | 195 | // create new sobject for updating the record 196 | SObject newSObject = new SObject(); 197 | newSObject.setType("ApexTestQueueItem"); 198 | newSObject.setField("Id", id); 199 | // abort the test using DML, set status to 200 | // "Aborted" 201 | newSObject.setField("Status", "Aborted"); 202 | totalTestClassesAborted++; 203 | // logging the status and id fields to compare 204 | // them for pre and post update call 205 | 206 | try { 207 | // TODO : up to 10 records can be updated at 208 | // a time by update() call. 209 | // add the logic to leverage this feature. 210 | // Currently only one record is being 211 | // updated(aborted) 212 | 213 | // Challenge: By the time we wait for 10 214 | // records that needs to be aborted, the 215 | // 'to-be-aborted' test might continue to 216 | // run and might get completed 217 | 218 | // update() call- analogous to UPDATE 219 | // Statement in SQL 220 | SaveResult[] saveResults = conn.update(new SObject[] { newSObject }); 221 | 222 | LOG.debug("Stop time: " + stopWatch.getTime()); 223 | stopWatch.stop(); 224 | 225 | for (int i = 0; i < saveResults.length; i++) { 226 | if (saveResults[i].isSuccess()) { 227 | LOG.debug("The record " + saveResults[i].getId() 228 | + " was updated successfully"); 229 | LOG.info("Aborted test case: " + testName 230 | + " since the test took more time than the threshold execution time of " 231 | + CommandLineArguments.getMaxTestExecTimeThreshold() + " mins"); 232 | } else { 233 | // There were errors during the 234 | // update call, so loop through and 235 | // print them out 236 | 237 | StringBuffer errorMsg = new StringBuffer(); 238 | errorMsg.append("Record " + saveResults[i].getId() + " failed to save"); 239 | for (int j = 0; j < saveResults[i].getErrors().length; j++) { 240 | com.sforce.soap.partner.Error err = saveResults[i].getErrors()[j]; 241 | errorMsg.append("error code: " + err.getStatusCode().toString()); 242 | errorMsg.append("error message: " + err.getMessage()); 243 | } 244 | ApexUnitUtils.shutDownWithErrMsg(errorMsg.toString()); 245 | } 246 | } 247 | LOG.debug("After update--" + newSObject.getField("Status").toString()); 248 | break; 249 | } catch (ConnectionException e) { 250 | ApexUnitUtils 251 | .shutDownWithDebugLog(e, ConnectionHandler.logConnectionException(e, conn, soql)); 252 | } 253 | 254 | } 255 | 256 | LOG.debug("Status of the test class: " + testName + " (" + CommandLineArguments.getOrgUrl() 257 | + "/" + testId + " ) " + " is : " + status); 258 | 259 | while (stopWatch.getTime() % 1000 != 0) { 260 | // wait, till 1 second elapses 261 | } 262 | LOG.debug("Firing polling query at " + stopWatch.getTime()); 263 | queryResult = conn.query(soql); 264 | sObjects = queryResult.getRecords(); 265 | status = sObjects[index].getField("Status").toString(); 266 | } 267 | endTime = stopWatch.getTime(); 268 | // get and log extended status for the test 269 | if (sObjects[index] != null && sObjects[index].getField("ExtendedStatus") != null) { 270 | String extendedStatus = sObjects[index].getField("ExtendedStatus").toString(); 271 | LOG.info("Test status for " + testName + ":" + extendedStatus); 272 | } 273 | LOG.info("Completed executing the test class: " + testName + ". Time taken by the test: " 274 | + endTime / 1000 / 60 + " minutes," + (endTime / 1000) % 60 + " seconds"); 275 | index++; 276 | remainingTests = totalTests - index; 277 | LOG.info("Total tests executed " + index + " , Remaining tests " + remainingTests); 278 | if (remainingTests == 0) { 279 | testsCompleted = true; 280 | } 281 | } 282 | } 283 | } 284 | } catch (ConnectionException e) { 285 | ApexUnitUtils 286 | .shutDownWithDebugLog(e, ConnectionHandler.logConnectionException(e, conn, soql)); 287 | } 288 | return testsCompleted; 289 | 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/testEngine/AsyncBulkApiHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Bulk API handler class to deal with the bulk API's used in the bulk batch queries during the test execution 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.client.testEngine; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.util.ArrayList; 21 | import java.util.HashMap; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.Set; 26 | 27 | import org.apache.commons.lang.StringUtils; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import com.sforce.async.AsyncApiException; 32 | import com.sforce.async.BatchInfo; 33 | import com.sforce.async.BatchStateEnum; 34 | import com.sforce.async.BulkConnection; 35 | import com.sforce.async.CSVReader; 36 | import com.sforce.async.ContentType; 37 | import com.sforce.async.JobInfo; 38 | import com.sforce.async.JobStateEnum; 39 | import com.sforce.async.OperationEnum; 40 | import com.sforce.cd.apexUnit.ApexUnitUtils; 41 | import com.sforce.cd.apexUnit.client.QueryConstructor; 42 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 43 | import com.sforce.soap.partner.PartnerConnection; 44 | import com.sforce.soap.partner.QueryResult; 45 | import com.sforce.soap.partner.SaveResult; 46 | import com.sforce.soap.partner.sobject.SObject; 47 | import com.sforce.ws.ConnectionException; 48 | 49 | /* 50 | * This class handles the Bulk Query operations 51 | */ 52 | public class AsyncBulkApiHandler { 53 | 54 | private static Logger LOG = LoggerFactory.getLogger(AsyncBulkApiHandler.class); 55 | 56 | /* 57 | * This method handles the bulk API flow using BulkConnection: 1. Creates 58 | * JobInfo for the ApexTestQueueItem object 2. Creates a list of BatchInfo 59 | * using the csv file(bulk file) 3. Closes the JobInfo object 4. Waits for 60 | * completion of the batch jobs 5. Fetches the results from the Batch jobs 61 | * 6. Returns parentJobId for the batch job results 62 | */ 63 | public String handleBulkApiFlow(PartnerConnection conn, BulkConnection bulkConnection, 64 | String[] testClassesAsArray) { 65 | String parentJobId = ""; 66 | JobInfo job; 67 | try { 68 | 69 | // TODO : evaluate using builder pattern? 70 | // Creates JobInfo for the ApexTestQueueItem object 71 | job = createJob("ApexTestQueueItem", bulkConnection, OperationEnum.insert); 72 | // Creates a list of BatchInfo using the test Classes fetched 73 | 74 | List batchInfoList = createBatchesForApexClasses(bulkConnection, job, testClassesAsArray); 75 | 76 | // Closes the JobInfo object 77 | closeJob(bulkConnection, job.getId()); 78 | // Waits for completion of the batch jobs 79 | awaitCompletion(bulkConnection, job, batchInfoList); 80 | // Fetches the results from the Batch jobs 81 | List batchResults = checkResults(bulkConnection, job, batchInfoList); 82 | // Returns parentJobId for the batch job results 83 | if (batchResults != null) { 84 | parentJobId = getParentJobIdForTestQueueItems(batchResults, conn); 85 | } else { 86 | ApexUnitUtils.shutDownWithErrMsg( 87 | "Problem encountered while trying to fetch results of the batch job upon completion. " 88 | + "Null batchResult was returned"); 89 | } 90 | } catch (AsyncApiException e) { 91 | ApexUnitUtils 92 | .shutDownWithDebugLog(e, "Caught AsyncApiException exception while trying to deal with bulk connection: " 93 | + e.getMessage()); 94 | } catch (IOException e) { 95 | ApexUnitUtils 96 | .shutDownWithDebugLog(e, "Caught IO exception while trying to deal with bulk connection: " 97 | + e.getMessage()); 98 | } finally { 99 | bulkConnection = null; 100 | } 101 | if (parentJobId == null) { 102 | ApexUnitUtils.shutDownWithErrMsg("Parent Job Id returned is null. " 103 | + "This typically means the test classes were not submitted correctly to the (Force.com )test execution engine"); 104 | } 105 | LOG.info( 106 | "############################# List of Apex test classes successfully submitted to the Force.com test execution engine #############################"); 107 | return parentJobId; 108 | } 109 | 110 | public List createBatchesForApexClasses(BulkConnection bulkConnection, JobInfo jobInfo, 111 | String[] testClassesAsArray) { 112 | 113 | List batchInfos = new ArrayList(); 114 | String stringToFeedIntoTheBatch = "ApexClassId\n"; 115 | for (String testClass : testClassesAsArray) { 116 | stringToFeedIntoTheBatch += testClass; 117 | stringToFeedIntoTheBatch += "\n"; 118 | } 119 | InputStream inputStream = new ByteArrayInputStream(stringToFeedIntoTheBatch.getBytes()); 120 | 121 | batchInfos = createBatch(inputStream, batchInfos, jobInfo, bulkConnection); 122 | 123 | return batchInfos; 124 | } 125 | 126 | /** 127 | * Create a batchInfo by using the inputStream consisting of classIds. 128 | * 129 | * @param inputStream 130 | * The input stream used to create a batchInfo. 131 | * @param batchInfos 132 | * The batch info for the newly created batch is added to this 133 | * list. 134 | * @param bulkConnection 135 | * The BulkConnection used to create the new batch. 136 | * @param jobInfo 137 | * The JobInfo associated with the new batch. 138 | * @throws IOException 139 | */ 140 | private List createBatch(InputStream inputStream, List batchInfos, JobInfo jobInfo, 141 | BulkConnection bulkConnection) { 142 | 143 | try { 144 | LOG.info("Creating batch for the test classes to execute using bulk connection...."); 145 | BatchInfo batchInfo = bulkConnection.createBatchFromStream(jobInfo, inputStream); 146 | batchInfos.add(batchInfo); 147 | 148 | } catch (AsyncApiException e) { 149 | ApexUnitUtils 150 | .shutDownWithDebugLog(e, "Encountered AsyncApiException Exception while trying to create batchInfo" 151 | + " using bulk connection. " + e.getMessage()); 152 | } finally { 153 | try { 154 | inputStream.close(); 155 | } catch (IOException e) { 156 | ApexUnitUtils 157 | .shutDownWithDebugLog(e, "Encountered IO Exception while trying to close the input stream after " 158 | + "creating batchInfo using bulk connection. " 159 | + e.getMessage()); 160 | } 161 | } 162 | return batchInfos; 163 | } 164 | 165 | /** 166 | * Create a new job using the Bulk API. 167 | * 168 | * @param sobjectType 169 | * The object type being loaded, such as "Account" 170 | * @param connection 171 | * BulkConnection used to create the new job. 172 | * @param operation 173 | * operation to be performed - insert/update/query/upsert 174 | * @return The JobInfo for the new job. 175 | * @throws AsyncApiException 176 | */ 177 | public JobInfo createJob(String sobjectType, BulkConnection bulkConnection, OperationEnum operation) 178 | throws AsyncApiException { 179 | JobInfo job = new JobInfo(); 180 | job.setObject(sobjectType); 181 | job.setOperation(operation); 182 | job.setContentType(ContentType.CSV); 183 | job = bulkConnection.createJob(job); 184 | 185 | return job; 186 | } 187 | 188 | /* 189 | * Moves the JobInfo's status to 'closed' status 190 | */ 191 | public void closeJob(BulkConnection bulkConnection, String jobId) throws AsyncApiException { 192 | JobInfo job = new JobInfo(); 193 | job.setId(jobId); 194 | job.setState(JobStateEnum.Closed); 195 | bulkConnection.updateJob(job); 196 | } 197 | 198 | /** 199 | * Wait for a job to complete by polling the Bulk API. 200 | * 201 | * @param connection 202 | * BulkConnection used to check results. 203 | * @param job 204 | * The job awaiting completion. 205 | * @param batchInfoList 206 | * List of batches for this job. 207 | * @throws AsyncApiException 208 | */ 209 | public void awaitCompletion(BulkConnection connection, JobInfo job, List batchInfoList) 210 | throws AsyncApiException { 211 | long sleepTime = 0L; 212 | Set incompleteBatchInfos = new HashSet(); 213 | for (BatchInfo bi : batchInfoList) { 214 | incompleteBatchInfos.add(bi.getId()); 215 | } 216 | while (!incompleteBatchInfos.isEmpty()) { 217 | try { 218 | Thread.sleep(sleepTime); 219 | } catch (InterruptedException e) { 220 | ApexUnitUtils 221 | .shutDownWithDebugLog(e, "InterruptedException encountered while the thread was attempting to sleep"); 222 | } 223 | LOG.debug("Awaiting results... Batches remaining for processing: " + incompleteBatchInfos.size()); 224 | sleepTime = 10000L; 225 | BatchInfo[] statusList = connection.getBatchInfoList(job.getId()).getBatchInfo(); 226 | for (BatchInfo batchInfo : statusList) { 227 | // Retain the BatchInfo's which are in InProgress and Queued 228 | // status, 229 | // Remove the rest from the incompleteBatchInfos 230 | if (batchInfo.getState() == BatchStateEnum.Completed) { 231 | if (incompleteBatchInfos.remove(batchInfo.getId())) { 232 | LOG.debug("BATCH STATUS:" + batchInfo.getStateMessage()); 233 | } 234 | } else if (batchInfo.getState() == BatchStateEnum.NotProcessed) { 235 | LOG.info("Batch " + batchInfo.getId() + " did not process, terminating it"); 236 | incompleteBatchInfos.remove(batchInfo.getId()); 237 | } else if (batchInfo.getState() == BatchStateEnum.Failed) { 238 | ApexUnitUtils.shutDownWithErrMsg("BATCH STATUS:" + batchInfo.getStateMessage()); 239 | } 240 | } 241 | } 242 | } 243 | 244 | /** 245 | * Gets the results of the operation and checks for errors. 246 | */ 247 | public List checkResults(BulkConnection bulkConnection, JobInfo job, List batchInfoList) 248 | throws AsyncApiException, IOException { 249 | LOG.debug("Checking Results.... "); 250 | List saveResults = new ArrayList(); 251 | 252 | // batchInfoList was populated when batches were created and submitted 253 | for (BatchInfo batchInfo : batchInfoList) { 254 | CSVReader csvReaderForBatchResultStream = new CSVReader( 255 | bulkConnection.getBatchResultStream(job.getId(), batchInfo.getId())); 256 | List resultHeader = csvReaderForBatchResultStream.nextRecord(); 257 | int resultCols = resultHeader.size(); 258 | 259 | List batchResultStream = null; 260 | while ((batchResultStream = csvReaderForBatchResultStream.nextRecord()) != null) { 261 | 262 | Map resultInfo = new HashMap(); 263 | for (int i = 0; i < resultCols; i++) { 264 | resultInfo.put(resultHeader.get(i), batchResultStream.get(i)); 265 | } 266 | SaveResult sr = new SaveResult(); 267 | sr.setId(resultInfo.get("Id")); 268 | boolean success = Boolean.valueOf(resultInfo.get("Success")); 269 | sr.setSuccess(success); 270 | 271 | if (!success) { 272 | if (resultInfo.get("Error") != null && StringUtils.isNotEmpty(resultInfo.get("Error"))) { 273 | ApexUnitUtils.shutDownWithErrMsg( 274 | "Error while fetching results for the batch job" + resultInfo.get("Error")); 275 | } 276 | } 277 | 278 | saveResults.add(sr); 279 | } 280 | } 281 | 282 | return saveResults; 283 | } 284 | 285 | /* 286 | * Fetches the parentJobId for the bulk results 287 | */ 288 | public String getParentJobIdForTestQueueItems(List bulkResults, PartnerConnection conn) { 289 | 290 | String parentJobId = null; 291 | 292 | if (bulkResults != null && bulkResults.size() > 0) { 293 | SaveResult sr = bulkResults.get(0); 294 | String testQueueItemId = sr.getId(); 295 | String soql = QueryConstructor.fetchParentJobIdForApexTestQueueItem(testQueueItemId); 296 | LOG.debug("Query used for fetching parent job ID for bulk results: " + soql); 297 | QueryResult queryResult = null; 298 | try{ 299 | queryResult = conn.query(soql); 300 | }catch (ConnectionException e) { 301 | ApexUnitUtils.shutDownWithDebugLog(e, ConnectionHandler 302 | .logConnectionException(e, conn, soql)); 303 | } 304 | if (queryResult.isDone()) { 305 | // TODO: We need to verify what's the limit of records that the 306 | // bulk api can insert in one transaction. multiple transactions 307 | // mean multiple parent job ids 308 | parentJobId = fetchParentJobId(queryResult); 309 | LOG.info("Async test parent job Id : " + parentJobId); 310 | } 311 | } else { 312 | ApexUnitUtils.shutDownWithErrMsg("Invalid bulk results. No bulk results returned."); 313 | } 314 | 315 | return parentJobId; 316 | } 317 | 318 | private String fetchParentJobId(QueryResult queryResult) { 319 | String parentJobId = ""; 320 | if (queryResult.getDone()) { 321 | SObject[] sObjects = queryResult.getRecords(); 322 | if (sObjects != null) { 323 | for (SObject sobject : sObjects) { 324 | parentJobId = sobject.getField("ParentJobId").toString(); 325 | } 326 | return parentJobId; 327 | } 328 | } 329 | return null; 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/report/ApexCodeCoverageReportGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class for generating code coverage reportfor a given ApexUnit run 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | 15 | package com.sforce.cd.apexUnit.report; 16 | 17 | import java.io.File; 18 | import java.io.FileNotFoundException; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | import java.io.UnsupportedEncodingException; 22 | import java.util.List; 23 | 24 | import com.sforce.cd.apexUnit.ApexUnitUtils; 25 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 26 | import com.sforce.cd.apexUnit.client.fileReader.ApexManifestFileReader; 27 | import com.sforce.cd.apexUnit.client.testEngine.TestStatusPollerAndResultHandler; 28 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 29 | 30 | public class ApexCodeCoverageReportGenerator { 31 | 32 | public static void generateHTMLReport(ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans) { 33 | // Preparing the table: 34 | StringBuilder htmlBuilder = new StringBuilder(); 35 | htmlBuilder.append(""); 36 | // String styleTagProperties = 37 | // "table { font-size: 18px; background-color: blue; color: orange; 38 | // text-align: center; }" 39 | // + 40 | // "body { font-family: \"Times New Roman\"; text-align: center; 41 | // font-size: 20px;}"; 42 | // appendTag(htmlBuilder, "style", styleTagProperties, ""); 43 | 44 | // Print a summary of the coverage for the team's apex classes and 45 | // triggers 46 | StringBuilder styleBuilder = new StringBuilder(); 47 | // define all styles for the html tags here 48 | String styleBuilderString = "body {background-color:white;} " + "h1 {color:blue; font-size:300%}" 49 | + "summary {color:black; font-size:125%;}" + "header {color:blue; font-size:200%;}" 50 | + "th {color:blue; font-size:125%; background-color:lightgrey;}"; 51 | appendTag(styleBuilder, "style", styleBuilderString); 52 | htmlBuilder.append(styleBuilder); 53 | StringBuilder summaryHeader = new StringBuilder(); 54 | 55 | String summaryHeaderString = "ApexUnit Report\n"; 56 | appendTag(summaryHeader, "h1", "align = 'center'; font-size: 25px; ", summaryHeaderString); 57 | appendLineSpaces(summaryHeader, 2); 58 | htmlBuilder.append(summaryHeader); 59 | 60 | StringBuilder codeCoverageSummary = new StringBuilder(); 61 | 62 | appendTag(codeCoverageSummary, "header", "Code Coverage Summary: *"); 63 | appendLineSpaces(codeCoverageSummary, 2); 64 | String teamCodeCoverageSummaryString = " Team code coverage: " 65 | + String.format("%.2f", ApexUnitCodeCoverageResults.teamCodeCoverage) + "%" 66 | + " [The customized team code coverage threshold was: " 67 | + CommandLineArguments.getTeamCodeCoverageThreshold() + "%]"; 68 | String orgWideCodeCoverageSummaryString = "
Org wide code coverage: " 69 | + String.format("%.2f", ApexUnitCodeCoverageResults.orgWideCodeCoverage) + "%" 70 | + " [The customized org wide code coverage threshold was: " 71 | + CommandLineArguments.getOrgWideCodeCoverageThreshold() + "%]"; 72 | if (ApexUnitCodeCoverageResults.teamCodeCoverage < CommandLineArguments.getTeamCodeCoverageThreshold()) { 73 | appendTag(codeCoverageSummary, "summary", "style=\"color:crimson\"", teamCodeCoverageSummaryString); 74 | } else { 75 | appendTag(codeCoverageSummary, "summary", teamCodeCoverageSummaryString); 76 | } 77 | if (ApexUnitCodeCoverageResults.orgWideCodeCoverage < CommandLineArguments.getOrgWideCodeCoverageThreshold()) { 78 | appendTag(codeCoverageSummary, "summary", "style=\"color:crimson\"", orgWideCodeCoverageSummaryString); 79 | } else { 80 | appendTag(codeCoverageSummary, "summary", orgWideCodeCoverageSummaryString); 81 | } 82 | appendLineSpaces(codeCoverageSummary, 2); 83 | 84 | htmlBuilder.append(codeCoverageSummary); 85 | 86 | StringBuilder apexTestExecutionSummary = new StringBuilder(); 87 | appendTag(apexTestExecutionSummary, "header", "Test Execution Summary: "); 88 | appendLineSpaces(apexTestExecutionSummary, 2); 89 | int failureTestMethodsCount = 0; 90 | if (TestStatusPollerAndResultHandler.testFailures) { 91 | if (TestStatusPollerAndResultHandler.failedTestMethods != null 92 | && !TestStatusPollerAndResultHandler.failedTestMethods.isEmpty()) 93 | failureTestMethodsCount = TestStatusPollerAndResultHandler.failedTestMethods.size(); 94 | } 95 | StringBuffer apexTestExecutionSummaryString = new StringBuffer( 96 | " Total test classes executed: " + TestStatusPollerAndResultHandler.totalTestClasses); 97 | if (TestStatusPollerAndResultHandler.totalTestClassesAborted > 0) { 98 | apexTestExecutionSummaryString.append("
Total Apex test classes aborted: " 99 | + TestStatusPollerAndResultHandler.totalTestClassesAborted); 100 | } 101 | apexTestExecutionSummaryString.append( 102 | "
Total test methods executed: " + TestStatusPollerAndResultHandler.totalTestMethodsExecuted); 103 | apexTestExecutionSummaryString.append("
Test method pass count: " 104 | + (TestStatusPollerAndResultHandler.totalTestMethodsExecuted - failureTestMethodsCount)); 105 | apexTestExecutionSummaryString.append("
Test method fail count: " + failureTestMethodsCount); 106 | 107 | appendTag(apexTestExecutionSummary, "summary", apexTestExecutionSummaryString.toString()); 108 | appendLineSpaces(apexTestExecutionSummary, 1); 109 | 110 | htmlBuilder.append(apexTestExecutionSummary); 111 | appendLineSpaces(htmlBuilder, 2); 112 | // provide link to the test report 113 | appendTag(htmlBuilder, "header", "Apex Test Report: "); 114 | appendLineSpaces(htmlBuilder, 2); 115 | String workingDir = System.getProperty("user.dir"); 116 | String apexUnitTestReportPath = ""; 117 | if (!workingDir.contains("jenkins")) { 118 | apexUnitTestReportPath = workingDir + System.getProperty("file.separator") + "ApexUnitReport.xml"; 119 | } else { 120 | int lastIndexOfSlash = workingDir.lastIndexOf('/'); 121 | String jobName = workingDir.substring(lastIndexOfSlash + 1); 122 | apexUnitTestReportPath = "https://jenkins.internal.salesforce.com/job/" + jobName 123 | + "/lastCompletedBuild/testReport/"; 124 | } 125 | appendTag(htmlBuilder, "a", "style=\"font-size:125%\"; href=" + apexUnitTestReportPath, "Detailed Test Report"); 126 | appendLineSpaces(htmlBuilder, 2); 127 | 128 | appendTag(htmlBuilder, "header", "Detailed code coverage report: "); 129 | appendLineSpaces(htmlBuilder, 2); 130 | htmlBuilder.append(""); 131 | 132 | StringBuilder codeCoverageHTMLContent = new StringBuilder(); 133 | if (apexClassCodeCoverageBeans != null) { 134 | // populate the header cells for the table 135 | codeCoverageHTMLContent.append("
"); 136 | appendHeaderCell(codeCoverageHTMLContent, "", "Apex Class Name"); 137 | appendHeaderCell(codeCoverageHTMLContent, "", "API Version"); 138 | appendHeaderCell(codeCoverageHTMLContent, "", "Code Coverage %"); 139 | appendHeaderCell(codeCoverageHTMLContent, "", "#Covered Lines"); 140 | appendHeaderCell(codeCoverageHTMLContent, "", "#Uncovered Lines"); 141 | appendHeaderCell(codeCoverageHTMLContent, "", "Covered Lines"); 142 | appendHeaderCell(codeCoverageHTMLContent, "", "Uncovered Lines"); 143 | appendHeaderCell(codeCoverageHTMLContent, "", "Length Without Comments(Bytes)"); 144 | codeCoverageHTMLContent.append("
"); 145 | appendTag(codeCoverageHTMLContent, "tr", ""); 146 | codeCoverageHTMLContent.append(""); 147 | // populate the data cells for the table 148 | for (ApexClassCodeCoverageBean apexClassCodeCoverageBean : apexClassCodeCoverageBeans) { 149 | String codeCoverageStyle = ""; 150 | if (apexClassCodeCoverageBean.getCoveragePercentage() < CommandLineArguments 151 | .getTeamCodeCoverageThreshold()) { 152 | codeCoverageStyle = "align='Center' style=\"color:crimson\""; 153 | } else { 154 | codeCoverageStyle = "align='Center' style=\"color:green\""; 155 | } 156 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 157 | apexClassCodeCoverageBean.getApexClassName()); 158 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, apexClassCodeCoverageBean.getApiVersion()); 159 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 160 | String.format("%.2f", apexClassCodeCoverageBean.getCoveragePercentage()) + "%"); 161 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 162 | "" + apexClassCodeCoverageBean.getNumLinesCovered()); 163 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 164 | "" + apexClassCodeCoverageBean.getNumLinesUncovered()); 165 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 166 | populateListInAStringBuffer(apexClassCodeCoverageBean.getCoveredLinesList())); 167 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 168 | populateListInAStringBuffer(apexClassCodeCoverageBean.getUncoveredLinesList())); 169 | appendDataCell(codeCoverageHTMLContent, codeCoverageStyle, 170 | apexClassCodeCoverageBean.getLengthWithoutComments()); 171 | appendTag(codeCoverageHTMLContent, "tr", ""); 172 | } 173 | codeCoverageHTMLContent.append(""); 174 | } 175 | htmlBuilder.append(""); 176 | htmlBuilder.append(codeCoverageHTMLContent); 177 | htmlBuilder.append("
"); 178 | 179 | // list out the duplicate entries(if any) 180 | if (ApexClassFetcherUtils.duplicateApexClassMap != null 181 | && ApexClassFetcherUtils.duplicateApexClassMap.size() > 0) { 182 | StringBuilder duplicateApexClassesHTMLContent = new StringBuilder(); 183 | appendLineSpaces(duplicateApexClassesHTMLContent, 2); 184 | duplicateApexClassesHTMLContent.append(""); 185 | appendHeaderCell(duplicateApexClassesHTMLContent, "", 186 | "Duplicate Apex Class Names Across Manifest Files And Regular Expressions"); 187 | for (String duplicateEntry : ApexClassFetcherUtils.duplicateApexClassMap.values()) { 188 | appendTag(duplicateApexClassesHTMLContent, "tr", ""); 189 | appendDataCell(duplicateApexClassesHTMLContent, "", duplicateEntry); 190 | } 191 | duplicateApexClassesHTMLContent.append("
"); 192 | htmlBuilder.append(duplicateApexClassesHTMLContent); 193 | } 194 | 195 | // list out the non existant class entries(if any) 196 | if (ApexManifestFileReader.nonExistantApexClassEntries != null 197 | && ApexManifestFileReader.nonExistantApexClassEntries.size() > 0) { 198 | StringBuilder nonExistantApexClassesHTMLContent = new StringBuilder(); 199 | appendLineSpaces(nonExistantApexClassesHTMLContent, 2); 200 | nonExistantApexClassesHTMLContent.append(""); 201 | appendHeaderCell(nonExistantApexClassesHTMLContent, "", 202 | "Invalid/Non-existant Apex Class Names Across Manifest Files And Regular Expressions"); 203 | for (String invalidEntry : ApexManifestFileReader.nonExistantApexClassEntries) { 204 | appendTag(nonExistantApexClassesHTMLContent, "tr", ""); 205 | appendDataCell(nonExistantApexClassesHTMLContent, "", invalidEntry); 206 | } 207 | nonExistantApexClassesHTMLContent.append("
"); 208 | htmlBuilder.append(nonExistantApexClassesHTMLContent); 209 | } 210 | appendLineSpaces(htmlBuilder, 2); 211 | appendTag(htmlBuilder, "a", 212 | "href=" + "http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_code_coverage_best_pract.htm", 213 | "Apex_Code_Coverage_Best_Practices"); 214 | htmlBuilder.append( 215 | "

* Code coverage is calculated by dividing the number of unique Apex code lines executed during your test method execution by the total number of Apex code lines in all of your trigger and classes.
(Note: these numbers do not include lines of code within your testMethods)
"); 216 | htmlBuilder.append(""); 217 | htmlBuilder.append(""); 218 | 219 | createHTMLReport(htmlBuilder.toString()); 220 | } 221 | 222 | private static void createHTMLReport(String htmlBuffer) { 223 | 224 | File tmpFile = null; 225 | FileOutputStream tmpOut = null; 226 | String workingDir = System.getProperty("user.dir") + System.getProperty("file.separator") + "Report"; 227 | File dir = new File(workingDir); 228 | dir.mkdirs(); 229 | tmpFile = new File(dir, "ApexUnitReport.html"); 230 | byte[] reportAsBytes; 231 | try { 232 | tmpOut = new FileOutputStream(tmpFile); 233 | reportAsBytes = htmlBuffer.getBytes("UTF-8"); 234 | tmpOut.write(reportAsBytes); 235 | tmpOut.close(); 236 | } catch (UnsupportedEncodingException e) { 237 | ApexUnitUtils 238 | .shutDownWithDebugLog(e, "UnsupportedEncodingException encountered while creating the HTML report"); 239 | } catch (FileNotFoundException e) { 240 | ApexUnitUtils 241 | .shutDownWithDebugLog(e, "FileNotFoundException encountered while writing the HTML report to ApexUnitReport.html"); 242 | } catch (IOException e) { 243 | ApexUnitUtils 244 | .shutDownWithDebugLog(e, "IOException encountered while writing the HTML report to ApexUnitReport.html"); 245 | } 246 | } 247 | 248 | private static String populateListInAStringBuffer(List listWithValues) { 249 | StringBuffer processedListAsStrBuf = new StringBuffer(""); 250 | int i = 0; 251 | if (listWithValues != null) { 252 | for (Long value : listWithValues) { 253 | i++; 254 | processedListAsStrBuf.append(value); 255 | processedListAsStrBuf.append(","); 256 | if (i >= 10) { 257 | processedListAsStrBuf.append("\n"); 258 | i = 0; 259 | } 260 | } 261 | return processedListAsStrBuf.substring(0, processedListAsStrBuf.length() - 1); 262 | } else { 263 | return "-"; 264 | } 265 | } 266 | 267 | private static void appendTag(StringBuilder sb, String tag, String tagProperties, String contents) { 268 | sb.append('<').append(tag).append(" " + tagProperties).append('>'); 269 | sb.append(contents); 270 | sb.append("'); 271 | } 272 | 273 | private static void appendTag(StringBuilder sb, String tag, String contents) { 274 | appendTag(sb, tag, "", contents); 275 | } 276 | 277 | private static void appendDataCell(StringBuilder sb, String tagProperties, String contents) { 278 | appendTag(sb, "td", tagProperties, contents); 279 | } 280 | 281 | private static void appendHeaderCell(StringBuilder sb, String tagProperties, String contents) { 282 | appendTag(sb, "th", tagProperties, contents); 283 | } 284 | 285 | private static void appendLineSpaces(StringBuilder sb, int numberOfLines) { 286 | for (int i = 1; i < numberOfLines; i++) { 287 | appendTag(sb, "br", ""); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/main/java/com/sforce/cd/apexUnit/client/codeCoverage/CodeCoverageComputer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, salesforce.com, inc. 3 | * All rights reserved. 4 | *Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | /* 9 | * Class to compute code coverage for the given class names and org wide code coverage 10 | * 11 | * @author adarsh.ramakrishna@salesforce.com 12 | */ 13 | 14 | package com.sforce.cd.apexUnit.client.codeCoverage; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Properties; 23 | import java.util.concurrent.CompletionService; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.ExecutorCompletionService; 26 | import java.util.concurrent.ExecutorService; 27 | import java.util.concurrent.Executors; 28 | 29 | import org.json.simple.JSONArray; 30 | import org.json.simple.JSONObject; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | import com.sforce.cd.apexUnit.ApexUnitUtils; 35 | import com.sforce.cd.apexUnit.arguments.CommandLineArguments; 36 | import com.sforce.cd.apexUnit.client.QueryConstructor; 37 | import com.sforce.cd.apexUnit.client.connection.ConnectionHandler; 38 | import com.sforce.cd.apexUnit.client.utils.ApexClassFetcherUtils; 39 | import com.sforce.cd.apexUnit.report.ApexClassCodeCoverageBean; 40 | import com.sforce.cd.apexUnit.report.ApexUnitCodeCoverageResults; 41 | import com.sforce.soap.partner.PartnerConnection; 42 | 43 | public class CodeCoverageComputer { 44 | private static Logger LOG = LoggerFactory.getLogger(CodeCoverageComputer.class); 45 | Properties prop = new Properties(); 46 | String propFileName = "config.properties"; 47 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(propFileName); 48 | private String SUPPORTED_VERSION = System.getProperty("API_VERSION"); 49 | private final int BATCH_SIZE = 100; 50 | 51 | /* 52 | * Constructor for CodeCoverageComputer Initialize SUPPORTED_VERSION 53 | * variable from property file TODO fetch SUPPORTED_VERSION from the org(by 54 | * querying?) 55 | */ 56 | public CodeCoverageComputer() { 57 | // execute below code Only when System.getProperty() call doesn't 58 | // function 59 | if (SUPPORTED_VERSION == null) { 60 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(propFileName); 61 | if (inputStream != null) { 62 | try { 63 | prop.load(inputStream); 64 | } catch (IOException e) { 65 | ApexUnitUtils 66 | .shutDownWithErrMsg("IO exception encountered while reading from the file:" + propFileName); 67 | } 68 | } 69 | SUPPORTED_VERSION = prop.getProperty("API_VERSION"); 70 | } 71 | } 72 | 73 | /** 74 | * Calculate Aggregated code coverage results for the Apex classes using 75 | * Tooling API's 76 | * 77 | * @return code coverage result(beans) as array 78 | */ 79 | @SuppressWarnings("unchecked") 80 | public ApexClassCodeCoverageBean[] calculateAggregatedCodeCoverageUsingToolingAPI() { 81 | PartnerConnection connection = ConnectionHandler.getConnectionHandlerInstance().getConnection(); 82 | 83 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = null; 84 | String[] classesAsArray = null; 85 | 86 | /* 87 | * Builder design pattern construct the test class array by building the 88 | * final array using simple objects(arrays) viz. array from Manifest 89 | * file and array from regex prefix 90 | */ 91 | // read class names from manifest files 92 | if (CommandLineArguments.getClassManifestFiles() != null) { 93 | LOG.debug(" Fetching apex classes from location : " + CommandLineArguments.getClassManifestFiles()); 94 | classesAsArray = ApexClassFetcherUtils 95 | .fetchApexClassesFromManifestFiles(CommandLineArguments.getClassManifestFiles(),true); 96 | } 97 | // fetch matching class names based on regex 98 | if (CommandLineArguments.getSourceRegex() != null) { 99 | LOG.debug(" Fetching apex classes with regex : " + CommandLineArguments.getSourceRegex()); 100 | classesAsArray = ApexClassFetcherUtils.fetchApexClassesBasedOnMultipleRegexes(connection, classesAsArray, 101 | CommandLineArguments.getSourceRegex(),true); 102 | } 103 | // Do not proceed if no class names are returned from both manifest 104 | // files and/or regexes 105 | if (classesAsArray != null && classesAsArray.length > 0) { 106 | 107 | if (classesAsArray.length > BATCH_SIZE) { 108 | // Creating multiple threads for sending request if URL is huge. 109 | 110 | ExecutorService threadPool = Executors.newFixedThreadPool(5); 111 | CompletionService pool = new ExecutorCompletionService(threadPool); 112 | 113 | int numOfBatches = 1; 114 | int fromIndex = 0; 115 | int toIndex = BATCH_SIZE; 116 | JSONArray recordObject = new JSONArray(); 117 | JSONObject responseJsonObject = null; 118 | LOG.info("Total number of classes: " + classesAsArray.length); 119 | 120 | if (classesAsArray.length % BATCH_SIZE == 0) { 121 | numOfBatches = classesAsArray.length / BATCH_SIZE; 122 | } else { 123 | numOfBatches = classesAsArray.length / BATCH_SIZE + 1; 124 | } 125 | 126 | for (int count = 0; count < numOfBatches; count++) { 127 | String[] ClassesInBatch = Arrays.copyOfRange(classesAsArray, fromIndex, toIndex); 128 | String classArrayAsStringForQuery = processClassArrayForQuery(ClassesInBatch); 129 | LOG.debug("Classes i nthis query: " + classArrayAsStringForQuery); 130 | LOG.info("Total number of classes in this query: " + ClassesInBatch.length); 131 | String relativeServiceURL = "/services/data/v" + SUPPORTED_VERSION + "/tooling"; 132 | // compute aggregated code coverage 133 | String soqlcc = QueryConstructor.getAggregatedCodeCoverage(classArrayAsStringForQuery); 134 | pool.submit(new CodeCoverageTask(relativeServiceURL, soqlcc, OAuthTokenGenerator.getOrgToken())); 135 | 136 | if (toIndex == classesAsArray.length) { 137 | break; 138 | } else { 139 | fromIndex = fromIndex + BATCH_SIZE; 140 | if ((toIndex + BATCH_SIZE) < (classesAsArray.length)) { 141 | toIndex = toIndex + BATCH_SIZE; 142 | } else { 143 | toIndex = classesAsArray.length; 144 | } 145 | } 146 | 147 | } 148 | 149 | for (int i = 0; i < numOfBatches; i++) { 150 | try { 151 | recordObject.addAll((JSONArray) pool.take().get().get("records")); 152 | } catch (InterruptedException e) { 153 | e.printStackTrace(); 154 | } catch (ExecutionException e) { 155 | e.printStackTrace(); 156 | } 157 | 158 | } 159 | 160 | threadPool.shutdown(); 161 | 162 | if (recordObject.size() > 0) { 163 | apexClassCodeCoverageBeans = processJSONResponseAndConstructCodeCoverageBeans(connection, 164 | recordObject); 165 | } 166 | 167 | } else { 168 | String classArrayAsStringForQuery = processClassArrayForQuery(classesAsArray); 169 | String relativeServiceURL = "/services/data/v" + SUPPORTED_VERSION + "/tooling"; 170 | // compute aggregated code coverage 171 | String soqlcc = QueryConstructor.getAggregatedCodeCoverage(classArrayAsStringForQuery); 172 | 173 | JSONObject responseJsonObject = null; 174 | responseJsonObject = WebServiceInvoker.doGet(relativeServiceURL, soqlcc, 175 | OAuthTokenGenerator.getOrgToken()); 176 | LOG.debug("responseJsonObject says " + responseJsonObject + "\n relativeServiceURL is " 177 | + relativeServiceURL + "\n soqlcc is " + soqlcc); 178 | if (responseJsonObject != null) { 179 | apexClassCodeCoverageBeans = processJSONResponseAndConstructCodeCoverageBeans(connection, 180 | (JSONArray) responseJsonObject.get("records")); 181 | } 182 | if (apexClassCodeCoverageBeans == null) { 183 | ApexUnitUtils.shutDownWithErrMsg( 184 | "Code coverage metrics not computed. Null object returned while processing the JSON response from the Tooling API"); 185 | } 186 | } 187 | } else { 188 | ApexUnitUtils.shutDownWithErrMsg("No/Invalid Apex source classes mentioned in manifest file and/or " 189 | + "regex pattern for ApexSourceClassPrefix didn't return any Apex source class names from the org"); 190 | } 191 | return apexClassCodeCoverageBeans; 192 | } 193 | 194 | /* 195 | * convert string array into csv string to facilitate the querying 196 | * 197 | * @param: class names as string array 198 | * 199 | * @return csv class names as string 200 | */ 201 | private String processClassArrayForQuery(String[] classesAsArray) { 202 | String queryString = ""; 203 | for (int i = 0; i < classesAsArray.length; i++) { 204 | queryString += "'" + classesAsArray[i] + "'"; 205 | queryString += ","; 206 | } 207 | if (queryString.length() > 1) { 208 | queryString = queryString.substring(0, queryString.length() - 1); 209 | } 210 | return queryString; 211 | } 212 | 213 | /* 214 | * Derive an array of code coverage beans from processing the JSON response 215 | * of Tooling API call 216 | * 217 | * @param connection = partner connection 218 | * 219 | * @param responseJsonObject - json response object that needs to be 220 | * processed 221 | * 222 | * @return code coverage result(beans) as array 223 | */ 224 | private ApexClassCodeCoverageBean[] processJSONResponseAndConstructCodeCoverageBeans(PartnerConnection connection, 225 | JSONArray aggregateRecordObject) { 226 | int classCounter = 0; 227 | int coveredLinesForTheTeam = 0; 228 | int unCoveredLinesForTheTeam = 0; 229 | JSONArray recordObject = aggregateRecordObject; 230 | if (recordObject != null && recordObject.size() > 0) { 231 | ApexClassCodeCoverageBean[] apexClassCodeCoverageBeans = new ApexClassCodeCoverageBean[recordObject.size()]; 232 | for (int i = 0; i < recordObject.size(); ++i) { 233 | 234 | ApexClassCodeCoverageBean apexClassCodeCoverageBean = new ApexClassCodeCoverageBean(); 235 | // The object below is one record from the ApexCodeCoverage 236 | // object 237 | JSONObject rec = (JSONObject) recordObject.get(i); 238 | 239 | // ApexClassOrTriggerId - The ID of the class or trigger under 240 | // test. 241 | String apexClassOrTriggerId = (String) rec.get("ApexClassOrTriggerId").toString(); 242 | if (apexClassOrTriggerId != null) { 243 | int coveredLines = 0; 244 | if (rec.get("NumLinesCovered") != null) { 245 | coveredLines = Integer.valueOf((String) rec.get("NumLinesCovered").toString()); 246 | coveredLinesForTheTeam += coveredLines; 247 | } else { 248 | LOG.debug(apexClassOrTriggerId + " has NumLinesCovered as NULL !!!!!!!!!"); 249 | } 250 | int unCoveredLines = 0; 251 | if (rec.get("NumLinesUncovered") != null) { 252 | unCoveredLines = Integer.valueOf((String) rec.get("NumLinesUncovered").toString()); 253 | unCoveredLinesForTheTeam += unCoveredLines; 254 | } else { 255 | LOG.debug(apexClassOrTriggerId + " has NumLinesUncovered as NULL !!!!!!!!!"); 256 | } 257 | if (rec.get("Coverage") != null) { 258 | JSONObject codeCoverageLineLists = (JSONObject) rec.get("Coverage"); 259 | JSONArray coveredLinesJsonArray = (JSONArray) codeCoverageLineLists.get("coveredLines"); 260 | JSONArray uncoveredLinesJsonArray = (JSONArray) codeCoverageLineLists.get("uncoveredLines"); 261 | List coveredLinesList = new ArrayList(); 262 | for (int j = 0; j < coveredLinesJsonArray.size(); j++) { 263 | coveredLinesList.add((Long) coveredLinesJsonArray.get(j)); 264 | LOG.debug("covered " + (Long) coveredLinesJsonArray.get(j)); 265 | } 266 | if (coveredLinesList != null && coveredLinesList.size() > 0) { 267 | apexClassCodeCoverageBean.setCoveredLinesList(coveredLinesList); 268 | } 269 | 270 | List uncoveredLinesList = new ArrayList(); 271 | for (int k = 0; k < uncoveredLinesJsonArray.size(); k++) { 272 | uncoveredLinesList.add((Long) uncoveredLinesJsonArray.get(k)); 273 | LOG.debug("uncovered " + (Long) uncoveredLinesJsonArray.get(k)); 274 | } 275 | if (uncoveredLinesList != null && uncoveredLinesList.size() > 0) { 276 | apexClassCodeCoverageBean.setUncoveredLinesList(uncoveredLinesList); 277 | } 278 | } 279 | 280 | apexClassCodeCoverageBean.setNumLinesCovered(coveredLines); 281 | apexClassCodeCoverageBean.setNumLinesUncovered(unCoveredLines); 282 | apexClassCodeCoverageBean.setApexClassorTriggerId(apexClassOrTriggerId); 283 | HashMap apexClassInfoMap = ApexClassFetcherUtils 284 | .fetchApexClassInfoFromId(connection, apexClassOrTriggerId); 285 | String apexClassName = apexClassInfoMap.get("Name"); 286 | String apiVersion = apexClassInfoMap.get("ApiVersion"); 287 | String lengthWithoutComments = apexClassInfoMap.get("LengthWithoutComments"); 288 | apexClassCodeCoverageBean.setApexClassName(apexClassName); 289 | apexClassCodeCoverageBean.setApiVersion(apiVersion); 290 | apexClassCodeCoverageBean.setLengthWithoutComments(lengthWithoutComments); 291 | apexClassCodeCoverageBeans[classCounter++] = apexClassCodeCoverageBean; 292 | 293 | LOG.info("Record number # " + classCounter + " : coveredLines : " + coveredLines 294 | + " : unCoveredLines : " + unCoveredLines + " : code coverage % : " 295 | + apexClassCodeCoverageBean.getCoveragePercentage() + " : apexClassOrTriggerId : " 296 | + apexClassOrTriggerId + " : apexClassName : " + apexClassName + " : apiVersion : " 297 | + apiVersion + " : lengthWithoutComments : " + lengthWithoutComments); 298 | } 299 | } 300 | double totalLines = coveredLinesForTheTeam + unCoveredLinesForTheTeam; 301 | if (totalLines > 0.0) { 302 | ApexUnitCodeCoverageResults.teamCodeCoverage = (coveredLinesForTheTeam / (totalLines)) * 100.0; 303 | } else { 304 | ApexUnitCodeCoverageResults.teamCodeCoverage = 100.0; 305 | } 306 | LOG.info( 307 | "#################################### Summary of code coverage computation for the team.. #################################### "); 308 | LOG.info("Total Covered lines : " + coveredLinesForTheTeam + "\n Total Uncovered lines : " 309 | + unCoveredLinesForTheTeam); 310 | 311 | LOG.info("Team code coverage is : " + ApexUnitCodeCoverageResults.teamCodeCoverage + "%"); 312 | return apexClassCodeCoverageBeans; 313 | } else { 314 | // no code coverage record object found in the response. return null 315 | return null; 316 | } 317 | } 318 | 319 | /** 320 | * This method is not used currently Calculate code coverage results for the 321 | * Apex classes using Tooling API's This method is intended to provide code 322 | * coverage at method level for each class . This indicates which exact 323 | * method needs more coverage 324 | * 325 | * @return 326 | */ 327 | public void calculateCodeCoverageUsingToolingAPI(String classArrayAsStringForQuery) { 328 | int classCounter = 0; 329 | String relativeServiceURL = "/services/data/v" + SUPPORTED_VERSION + "/tooling"; 330 | String soqlcc = QueryConstructor.getClassLevelCodeCoverage(classArrayAsStringForQuery); 331 | LOG.debug("OAuthTokenGenerator.getOrgToken() : " + OAuthTokenGenerator.getOrgToken()); 332 | JSONObject responseJsonObject = null; 333 | responseJsonObject = WebServiceInvoker.doGet(relativeServiceURL, soqlcc, OAuthTokenGenerator.getOrgToken()); 334 | 335 | if (responseJsonObject != null) { 336 | String responseStr = responseJsonObject.toJSONString(); 337 | LOG.debug(responseStr); 338 | JSONArray recordObject = (JSONArray) responseJsonObject.get("records"); 339 | for (int i = 0; i < recordObject.size(); ++i) { 340 | classCounter++; 341 | // The object below is one record from the ApexCodeCoverage 342 | // object 343 | JSONObject rec = (JSONObject) recordObject.get(i); 344 | 345 | int coveredLines = Integer.valueOf((String) rec.get("NumLinesCovered").toString()); 346 | int unCoveredLines = Integer.valueOf((String) rec.get("NumLinesUncovered").toString()); 347 | // ApexTestClassId - The ID of the test class. 348 | String apexTestClassID = (String) rec.get("ApexTestClassId").toString(); 349 | // ApexClassOrTriggerId - The ID of the class or trigger under 350 | // test. 351 | String apexClassorTriggerId = (String) rec.get("ApexClassOrTriggerId").toString(); 352 | String testMethodName = (String) rec.get("TestMethodName").toString(); 353 | LOG.info("Record number # " + classCounter + " : coveredLines : " + coveredLines 354 | + " : unCoveredLines : " + unCoveredLines + " : apexTestClassID : " + apexTestClassID 355 | + " : apexClassorTriggerId : " + apexClassorTriggerId + " : testMethodName : " 356 | + testMethodName); 357 | 358 | } 359 | 360 | } 361 | } 362 | 363 | /* 364 | * Calculate org wide code coverage results for all the Apex classes in the 365 | * org using Tooling API's 366 | * 367 | * @return org wide code coverage result as integer 368 | */ 369 | public int getOrgWideCodeCoverage() { 370 | String relativeServiceURL = "/services/data/v" + SUPPORTED_VERSION + "/tooling"; 371 | String soql = QueryConstructor.getOrgWideCoverage(); 372 | int coverage = 0; 373 | JSONObject responseJsonObject = null; 374 | responseJsonObject = WebServiceInvoker.doGet(relativeServiceURL, soql, OAuthTokenGenerator.getOrgToken()); 375 | 376 | if (responseJsonObject != null) { 377 | String responseStr = responseJsonObject.toJSONString(); 378 | LOG.debug("responseStr during org wide code coverage" + responseStr); 379 | JSONArray recordObject = (JSONArray) responseJsonObject.get("records"); 380 | for (int i = 0; i < recordObject.size(); ++i) { 381 | 382 | JSONObject rec = (JSONObject) recordObject.get(i); 383 | 384 | coverage = Integer.valueOf((String) rec.get("PercentCovered").toString()); 385 | LOG.info( 386 | "#################################### Org wide code coverage result #################################### "); 387 | LOG.info("Org wide code coverage : " + coverage + "%"); 388 | } 389 | } else { 390 | ApexUnitUtils.shutDownWithErrMsg("Org wide code coverage not computed"); 391 | } 392 | ApexUnitCodeCoverageResults.orgWideCodeCoverage = coverage; 393 | return coverage; 394 | } 395 | } --------------------------------------------------------------------------------