View Javadoc
1   /*
2    * Copyright 2013-2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.cloud.contract.maven.verifier;
18  
19  import java.io.File;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.apache.maven.execution.MavenSession;
25  import org.apache.maven.model.Dependency;
26  import org.apache.maven.model.Resource;
27  import org.apache.maven.plugin.AbstractMojo;
28  import org.apache.maven.plugin.MojoExecution;
29  import org.apache.maven.plugin.MojoExecutionException;
30  import org.apache.maven.plugin.MojoFailureException;
31  import org.apache.maven.plugins.annotations.LifecyclePhase;
32  import org.apache.maven.plugins.annotations.Mojo;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.plugins.annotations.ResolutionScope;
35  import org.apache.maven.project.MavenProject;
36  import org.eclipse.aether.RepositorySystemSession;
37  
38  import org.springframework.cloud.contract.spec.ContractVerifierException;
39  import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
40  import org.springframework.cloud.contract.verifier.TestGenerator;
41  import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties;
42  import org.springframework.cloud.contract.verifier.config.TestFramework;
43  import org.springframework.cloud.contract.verifier.config.TestMode;
44  import org.springframework.util.StringUtils;
45  
46  /**
47   * From the provided directory with contracts generates the acceptance tests on the
48   * producer side.
49   *
50   * @author Mariusz Smykula
51   */
52  @Mojo(name = "generateTests", defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES,
53  		requiresDependencyResolution = ResolutionScope.TEST)
54  public class GenerateTestsMojo extends AbstractMojo {
55  
56  	@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
57  	private RepositorySystemSession repoSession;
58  
59  	@Parameter(property = "spring.cloud.contract.verifier.contractsDirectory",
60  			defaultValue = "${project.basedir}/src/test/resources/contracts")
61  	private File contractsDirectory;
62  
63  	@Parameter(defaultValue = "${project.build.directory}/generated-test-sources/contracts")
64  	private File generatedTestSourcesDir;
65  
66  	@Parameter(defaultValue = "${project.build.directory}/generated-test-resources/contracts")
67  	private File generatedTestResourcesDir;
68  
69  	@Parameter
70  	private String basePackageForTests;
71  
72  	@Parameter
73  	private String baseClassForTests;
74  
75  	@Parameter(defaultValue = "MOCKMVC")
76  	private TestMode testMode;
77  
78  	@Parameter(defaultValue = "JUNIT5")
79  	private TestFramework testFramework;
80  
81  	@Parameter
82  	private String ruleClassForTests;
83  
84  	@Parameter
85  	private String nameSuffixForTests;
86  
87  	/**
88  	 * Imports that should be added to generated tests.
89  	 */
90  	@Parameter
91  	private String[] imports;
92  
93  	/**
94  	 * Static imports that should be added to generated tests.
95  	 */
96  	@Parameter
97  	private String[] staticImports;
98  
99  	/**
100 	 * Patterns that should not be taken into account for processing.
101 	 */
102 	@Parameter
103 	private List<String> excludedFiles;
104 
105 	/**
106 	 * Patterns that should be taken into account for processing.
107 	 */
108 	@Parameter(property = "includedFiles")
109 	private List<String> includedFiles;
110 
111 	/**
112 	 * Incubating feature. You can check the size of JSON arrays. If not turned on
113 	 * explicitly will be disabled.
114 	 */
115 	@Parameter(property = "spring.cloud.contract.verifier.assert.size", defaultValue = "false")
116 	private boolean assertJsonSize;
117 
118 	/**
119 	 * Patterns for which Spring Cloud Contract Verifier should generate @Ignored tests.
120 	 */
121 	@Parameter
122 	private List<String> ignoredFiles;
123 
124 	@Parameter(defaultValue = "${project}", readonly = true)
125 	private MavenProject project;
126 
127 	@Parameter(property = "spring.cloud.contract.verifier.skip", defaultValue = "false")
128 	private boolean skip;
129 
130 	@Parameter(property = "maven.test.skip", defaultValue = "false")
131 	private boolean mavenTestSkip;
132 
133 	/**
134 	 * The URL from which a contracts should get downloaded. If not provided but
135 	 * artifactid / coordinates notation was provided then the current Maven's build
136 	 * repositories will be taken into consideration.
137 	 */
138 	@Parameter(property = "contractsRepositoryUrl")
139 	private String contractsRepositoryUrl;
140 
141 	@Parameter(property = "contractDependency")
142 	private Dependency contractDependency;
143 
144 	/**
145 	 * The path in the JAR with all the contracts where contracts for this particular
146 	 * service lay. If not provided will be resolved to {@code groupid/artifactid}.
147 	 * Example: If {@code groupid} is {@code com.example} and {@code artifactid} is
148 	 * {@code service} then the resolved path will be {@code /com/example/artifactid}
149 	 */
150 	@Parameter(property = "contractsPath")
151 	private String contractsPath;
152 
153 	/**
154 	 * Picks the mode in which stubs will be found and registered.
155 	 */
156 	@Parameter(property = "contractsMode", defaultValue = "CLASSPATH")
157 	private StubRunnerProperties.StubsMode contractsMode;
158 
159 	/**
160 	 * A package that contains all the base clases for generated tests. If your contract
161 	 * resides in a location {@code src/test/resources/contracts/com/example/v1/} and you
162 	 * provide the {@code packageWithBaseClasses} value to
163 	 * {@code com.example.contracts.base} then we will search for a test source file that
164 	 * will have the package {@code com.example.contracts.base} and name
165 	 * {@code ExampleV1Base}. As you can see it will take the two last folders to and
166 	 * attach {@code Base} to its name.
167 	 */
168 	@Parameter(property = "packageWithBaseClasses")
169 	private String packageWithBaseClasses;
170 
171 	/**
172 	 * A way to override any base class mappings. The keys are regular expressions on the
173 	 * package name of the contract and the values FQN to a base class for that given
174 	 * expression. Example of a mapping {@code .*.com.example.v1..*} ->
175 	 * {@code com.example.SomeBaseClass} When a contract's package matches the provided
176 	 * regular expression then extending class will be the one provided in the map - in
177 	 * this case {@code com.example.SomeBaseClass}.
178 	 */
179 	@Parameter(property = "baseClassMappings")
180 	private List<BaseClassMapping> baseClassMappings;
181 
182 	/**
183 	 * The user name to be used to connect to the repo with contracts.
184 	 */
185 	@Parameter(property = "contractsRepositoryUsername")
186 	private String contractsRepositoryUsername;
187 
188 	/**
189 	 * The password to be used to connect to the repo with contracts.
190 	 */
191 	@Parameter(property = "contractsRepositoryPassword")
192 	private String contractsRepositoryPassword;
193 
194 	/**
195 	 * The proxy host to be used to connect to the repo with contracts.
196 	 */
197 	@Parameter(property = "contractsRepositoryProxyHost")
198 	private String contractsRepositoryProxyHost;
199 
200 	/**
201 	 * The proxy port to be used to connect to the repo with contracts.
202 	 */
203 	@Parameter(property = "contractsRepositoryProxyPort")
204 	private Integer contractsRepositoryProxyPort;
205 
206 	/**
207 	 * If set to {@code false} will NOT delete stubs from a temporary folder after running
208 	 * tests.
209 	 */
210 	@Parameter(property = "deleteStubsAfterTest", defaultValue = "true")
211 	private boolean deleteStubsAfterTest;
212 
213 	/**
214 	 * Map of properties that can be passed to custom
215 	 * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
216 	 */
217 	@Parameter(property = "contractsProperties")
218 	private Map<String, String> contractsProperties = new HashMap<>();
219 
220 	/**
221 	 * When enabled, this flag will tell stub runner to throw an exception when no stubs /
222 	 * contracts were found.
223 	 */
224 	@Parameter(property = "failOnNoContracts", defaultValue = "true")
225 	private boolean failOnNoContracts;
226 
227 	/**
228 	 * If set to true then if any contracts that are in progress are found, will break the
229 	 * build. On the producer side you need to be explicit about the fact that you have
230 	 * contracts in progress and take into consideration that you might be causing false
231 	 * positive test execution results on the consumer side.
232 	 */
233 	@Parameter(property = "failOnInProgress", defaultValue = "true")
234 	private boolean failOnInProgress = true;
235 
236 	/**
237 	 * If set to true then tests are created only when contracts have changed since last
238 	 * build.
239 	 */
240 	@Parameter(property = "incrementalContractTests", defaultValue = "true")
241 	private boolean incrementalContractTests = true;
242 
243 	@Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
244 	private MojoExecution mojoExecution;
245 
246 	@Parameter(defaultValue = "${session}", readonly = true, required = true)
247 	private MavenSession session;
248 
249 	@Override
250 	public void execute() throws MojoExecutionException, MojoFailureException {
251 		if (this.skip || this.mavenTestSkip) {
252 			if (this.skip) {
253 				getLog().info("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip="
254 						+ this.skip);
255 			}
256 			if (this.mavenTestSkip) {
257 				getLog().info(
258 						"Skipping Spring Cloud Contract Verifier execution: maven.test.skip=" + this.mavenTestSkip);
259 			}
260 			return;
261 		}
262 		getLog().info("Generating server tests source code for Spring Cloud Contract Verifier contract verification");
263 		final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
264 		config.setFailOnInProgress(this.failOnInProgress);
265 		// download contracts, unzip them and pass as output directory
266 		File contractsDirectory = new MavenContractsDownloader(this.project, this.contractDependency,
267 				this.contractsPath, this.contractsRepositoryUrl, this.contractsMode, getLog(),
268 				this.contractsRepositoryUsername, this.contractsRepositoryPassword, this.contractsRepositoryProxyHost,
269 				this.contractsRepositoryProxyPort, this.deleteStubsAfterTest, this.contractsProperties,
270 				this.failOnNoContracts).downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
271 		getLog().info("Directory with contract is present at [" + contractsDirectory + "]");
272 		throwExceptionWhenFailOnNoContracts(contractsDirectory, this.contractsRepositoryUrl);
273 
274 		if (this.incrementalContractTests
275 				&& !ChangeDetector.inputFilesChangeDetected(contractsDirectory, mojoExecution, session)) {
276 			getLog().info("Nothing to generate - all classes are up to date");
277 			return;
278 		}
279 
280 		setupConfig(config, contractsDirectory);
281 		this.project.addTestCompileSourceRoot(this.generatedTestSourcesDir.getAbsolutePath());
282 		Resource resource = new Resource();
283 		resource.setDirectory(this.generatedTestResourcesDir.getAbsolutePath());
284 		this.project.addTestResource(resource);
285 		if (getLog().isInfoEnabled()) {
286 			getLog().info("Test Source directory: " + this.generatedTestSourcesDir.getAbsolutePath() + " added.");
287 			getLog().info("Using [" + config.getBaseClassForTests() + "] as base class for test classes, ["
288 					+ config.getBasePackageForTests() + "] as base " + "package for tests, ["
289 					+ config.getPackageWithBaseClasses() + "] as package with " + "base classes, base class mappings "
290 					+ this.baseClassMappings);
291 		}
292 		try {
293 			LeftOverPrevention leftOverPrevention = new LeftOverPrevention(this.generatedTestSourcesDir, mojoExecution,
294 					session);
295 			TestGenerator generator = new TestGenerator(config);
296 			int generatedClasses = generator.generate();
297 			getLog().info("Generated " + generatedClasses + " test classes.");
298 			leftOverPrevention.deleteLeftOvers();
299 		}
300 		catch (ContractVerifierException e) {
301 			throw new MojoExecutionException(
302 					String.format("Spring Cloud Contract Verifier Plugin exception: %s", e.getMessage()), e);
303 		}
304 	}
305 
306 	private void throwExceptionWhenFailOnNoContracts(File file, String contractsRepository)
307 			throws MojoExecutionException {
308 		if (StringUtils.hasText(contractsRepository)) {
309 			if (getLog().isDebugEnabled()) {
310 				getLog().debug(
311 						"Contracts repository is set, will not throw an exception that the contracts are not found");
312 			}
313 			return;
314 		}
315 		if (this.failOnNoContracts && (!file.exists() || file.listFiles().length == 0)) {
316 			String path = file.getAbsolutePath();
317 			throw new MojoExecutionException("Contracts could not be found: [" + path
318 					+ "]\nPlease make sure that the contracts were defined, or set the [failOnNoContracts] property to [false]");
319 		}
320 	}
321 
322 	private void setupConfig(ContractVerifierConfigProperties config, File contractsDirectory) {
323 		config.setContractsDslDir(contractsDirectory);
324 		config.setGeneratedTestSourcesDir(this.generatedTestSourcesDir);
325 		config.setGeneratedTestResourcesDir(this.generatedTestResourcesDir);
326 		config.setTestFramework(this.testFramework);
327 		config.setTestMode(this.testMode);
328 		config.setBasePackageForTests(this.basePackageForTests);
329 		config.setBaseClassForTests(this.baseClassForTests);
330 		config.setRuleClassForTests(this.ruleClassForTests);
331 		config.setNameSuffixForTests(this.nameSuffixForTests);
332 		config.setImports(this.imports);
333 		config.setStaticImports(this.staticImports);
334 		config.setIgnoredFiles(this.ignoredFiles);
335 		config.setExcludedFiles(this.excludedFiles);
336 		config.setIncludedFiles(this.includedFiles);
337 		config.setAssertJsonSize(this.assertJsonSize);
338 		config.setPackageWithBaseClasses(this.packageWithBaseClasses);
339 		if (this.baseClassMappings != null) {
340 			config.setBaseClassMappings(mappingsToMap());
341 		}
342 	}
343 
344 	public Map<String, String> mappingsToMap() {
345 		Map<String, String> map = new HashMap<>();
346 		if (this.baseClassMappings == null) {
347 			return map;
348 		}
349 		for (BaseClassMapping mapping : this.baseClassMappings) {
350 			map.put(mapping.getContractPackageRegex(), mapping.getBaseClassFQN());
351 		}
352 		return map;
353 	}
354 
355 	public List<String> getExcludedFiles() {
356 		return this.excludedFiles;
357 	}
358 
359 	public void setExcludedFiles(List<String> excludedFiles) {
360 		this.excludedFiles = excludedFiles;
361 	}
362 
363 	public List<String> getIgnoredFiles() {
364 		return this.ignoredFiles;
365 	}
366 
367 	public void setIgnoredFiles(List<String> ignoredFiles) {
368 		this.ignoredFiles = ignoredFiles;
369 	}
370 
371 	public boolean isAssertJsonSize() {
372 		return this.assertJsonSize;
373 	}
374 
375 	public void setAssertJsonSize(boolean assertJsonSize) {
376 		this.assertJsonSize = assertJsonSize;
377 	}
378 
379 }