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 	@Parameter(property = "skipTests", defaultValue = "false")
134 	private boolean skipTests;
135 
136 	/**
137 	 * The URL from which a contracts should get downloaded. If not provided but
138 	 * artifactid / coordinates notation was provided then the current Maven's build
139 	 * repositories will be taken into consideration.
140 	 */
141 	@Parameter(property = "contractsRepositoryUrl")
142 	private String contractsRepositoryUrl;
143 
144 	@Parameter(property = "contractDependency")
145 	private Dependency contractDependency;
146 
147 	/**
148 	 * The path in the JAR with all the contracts where contracts for this particular
149 	 * service lay. If not provided will be resolved to {@code groupid/artifactid}.
150 	 * Example: If {@code groupid} is {@code com.example} and {@code artifactid} is
151 	 * {@code service} then the resolved path will be {@code /com/example/artifactid}
152 	 */
153 	@Parameter(property = "contractsPath")
154 	private String contractsPath;
155 
156 	/**
157 	 * Picks the mode in which stubs will be found and registered.
158 	 */
159 	@Parameter(property = "contractsMode", defaultValue = "CLASSPATH")
160 	private StubRunnerProperties.StubsMode contractsMode;
161 
162 	/**
163 	 * A package that contains all the base clases for generated tests. If your contract
164 	 * resides in a location {@code src/test/resources/contracts/com/example/v1/} and you
165 	 * provide the {@code packageWithBaseClasses} value to
166 	 * {@code com.example.contracts.base} then we will search for a test source file that
167 	 * will have the package {@code com.example.contracts.base} and name
168 	 * {@code ExampleV1Base}. As you can see it will take the two last folders to and
169 	 * attach {@code Base} to its name.
170 	 */
171 	@Parameter(property = "packageWithBaseClasses")
172 	private String packageWithBaseClasses;
173 
174 	/**
175 	 * A way to override any base class mappings. The keys are regular expressions on the
176 	 * package name of the contract and the values FQN to a base class for that given
177 	 * expression. Example of a mapping {@code .*.com.example.v1..*} ->
178 	 * {@code com.example.SomeBaseClass} When a contract's package matches the provided
179 	 * regular expression then extending class will be the one provided in the map - in
180 	 * this case {@code com.example.SomeBaseClass}.
181 	 */
182 	@Parameter(property = "baseClassMappings")
183 	private List<BaseClassMapping> baseClassMappings;
184 
185 	/**
186 	 * The user name to be used to connect to the repo with contracts.
187 	 */
188 	@Parameter(property = "contractsRepositoryUsername")
189 	private String contractsRepositoryUsername;
190 
191 	/**
192 	 * The password to be used to connect to the repo with contracts.
193 	 */
194 	@Parameter(property = "contractsRepositoryPassword")
195 	private String contractsRepositoryPassword;
196 
197 	/**
198 	 * The proxy host to be used to connect to the repo with contracts.
199 	 */
200 	@Parameter(property = "contractsRepositoryProxyHost")
201 	private String contractsRepositoryProxyHost;
202 
203 	/**
204 	 * The proxy port to be used to connect to the repo with contracts.
205 	 */
206 	@Parameter(property = "contractsRepositoryProxyPort")
207 	private Integer contractsRepositoryProxyPort;
208 
209 	/**
210 	 * If set to {@code false} will NOT delete stubs from a temporary folder after running
211 	 * tests.
212 	 */
213 	@Parameter(property = "deleteStubsAfterTest", defaultValue = "true")
214 	private boolean deleteStubsAfterTest;
215 
216 	/**
217 	 * Map of properties that can be passed to custom
218 	 * {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.
219 	 */
220 	@Parameter(property = "contractsProperties")
221 	private Map<String, String> contractsProperties = new HashMap<>();
222 
223 	/**
224 	 * When enabled, this flag will tell stub runner to throw an exception when no stubs /
225 	 * contracts were found.
226 	 */
227 	@Parameter(property = "failOnNoContracts", defaultValue = "true")
228 	private boolean failOnNoContracts;
229 
230 	/**
231 	 * If set to true then if any contracts that are in progress are found, will break the
232 	 * build. On the producer side you need to be explicit about the fact that you have
233 	 * contracts in progress and take into consideration that you might be causing false
234 	 * positive test execution results on the consumer side.
235 	 */
236 	@Parameter(property = "failOnInProgress", defaultValue = "true")
237 	private boolean failOnInProgress = true;
238 
239 	/**
240 	 * If set to true then tests are created only when contracts have changed since last
241 	 * build.
242 	 */
243 	@Parameter(property = "incrementalContractTests", defaultValue = "true")
244 	private boolean incrementalContractTests = true;
245 
246 	@Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
247 	private MojoExecution mojoExecution;
248 
249 	@Parameter(defaultValue = "${session}", readonly = true, required = true)
250 	private MavenSession session;
251 
252 	@Override
253 	public void execute() throws MojoExecutionException, MojoFailureException {
254 		if (this.skip || this.mavenTestSkip || this.skipTests) {
255 			if (this.skip) {
256 				getLog().info("Skipping Spring Cloud Contract Verifier execution: spring.cloud.contract.verifier.skip="
257 						+ this.skip);
258 			}
259 			if (this.mavenTestSkip) {
260 				getLog().info(
261 						"Skipping Spring Cloud Contract Verifier execution: maven.test.skip=" + this.mavenTestSkip);
262 			}
263 			if (this.skipTests) {
264 				getLog().info("Skipping Spring Cloud Contract Verifier execution: skipTests" + this.skipTests);
265 			}
266 			return;
267 		}
268 		getLog().info("Generating server tests source code for Spring Cloud Contract Verifier contract verification");
269 		final ContractVerifierConfigProperties config = new ContractVerifierConfigProperties();
270 		config.setFailOnInProgress(this.failOnInProgress);
271 		// download contracts, unzip them and pass as output directory
272 		File contractsDirectory = new MavenContractsDownloader(this.project, this.contractDependency,
273 				this.contractsPath, this.contractsRepositoryUrl, this.contractsMode, getLog(),
274 				this.contractsRepositoryUsername, this.contractsRepositoryPassword, this.contractsRepositoryProxyHost,
275 				this.contractsRepositoryProxyPort, this.deleteStubsAfterTest, this.contractsProperties,
276 				this.failOnNoContracts).downloadAndUnpackContractsIfRequired(config, this.contractsDirectory);
277 		getLog().info("Directory with contract is present at [" + contractsDirectory + "]");
278 		throwExceptionWhenFailOnNoContracts(contractsDirectory, this.contractsRepositoryUrl);
279 
280 		if (this.incrementalContractTests
281 				&& !ChangeDetector.inputFilesChangeDetected(contractsDirectory, mojoExecution, session)) {
282 			getLog().info("Nothing to generate - all classes are up to date");
283 			return;
284 		}
285 
286 		setupConfig(config, contractsDirectory);
287 		this.project.addTestCompileSourceRoot(this.generatedTestSourcesDir.getAbsolutePath());
288 		Resource resource = new Resource();
289 		resource.setDirectory(this.generatedTestResourcesDir.getAbsolutePath());
290 		this.project.addTestResource(resource);
291 		if (getLog().isInfoEnabled()) {
292 			getLog().info("Test Source directory: " + this.generatedTestSourcesDir.getAbsolutePath() + " added.");
293 			getLog().info("Using [" + config.getBaseClassForTests() + "] as base class for test classes, ["
294 					+ config.getBasePackageForTests() + "] as base " + "package for tests, ["
295 					+ config.getPackageWithBaseClasses() + "] as package with " + "base classes, base class mappings "
296 					+ this.baseClassMappings);
297 		}
298 		try {
299 			LeftOverPrevention leftOverPrevention = new LeftOverPrevention(this.generatedTestSourcesDir, mojoExecution,
300 					session);
301 			TestGenerator generator = new TestGenerator(config);
302 			int generatedClasses = generator.generate();
303 			getLog().info("Generated " + generatedClasses + " test classes.");
304 			leftOverPrevention.deleteLeftOvers();
305 		}
306 		catch (ContractVerifierException e) {
307 			throw new MojoExecutionException(
308 					String.format("Spring Cloud Contract Verifier Plugin exception: %s", e.getMessage()), e);
309 		}
310 	}
311 
312 	private void throwExceptionWhenFailOnNoContracts(File file, String contractsRepository)
313 			throws MojoExecutionException {
314 		if (StringUtils.hasText(contractsRepository)) {
315 			if (getLog().isDebugEnabled()) {
316 				getLog().debug(
317 						"Contracts repository is set, will not throw an exception that the contracts are not found");
318 			}
319 			return;
320 		}
321 		if (this.failOnNoContracts && (!file.exists() || file.listFiles().length == 0)) {
322 			String path = file.getAbsolutePath();
323 			throw new MojoExecutionException("Contracts could not be found: [" + path
324 					+ "]\nPlease make sure that the contracts were defined, or set the [failOnNoContracts] property to [false]");
325 		}
326 	}
327 
328 	private void setupConfig(ContractVerifierConfigProperties config, File contractsDirectory) {
329 		config.setContractsDslDir(contractsDirectory);
330 		config.setGeneratedTestSourcesDir(this.generatedTestSourcesDir);
331 		config.setGeneratedTestResourcesDir(this.generatedTestResourcesDir);
332 		config.setTestFramework(this.testFramework);
333 		config.setTestMode(this.testMode);
334 		config.setBasePackageForTests(this.basePackageForTests);
335 		config.setBaseClassForTests(this.baseClassForTests);
336 		config.setRuleClassForTests(this.ruleClassForTests);
337 		config.setNameSuffixForTests(this.nameSuffixForTests);
338 		config.setImports(this.imports);
339 		config.setStaticImports(this.staticImports);
340 		config.setIgnoredFiles(this.ignoredFiles);
341 		config.setExcludedFiles(this.excludedFiles);
342 		config.setIncludedFiles(this.includedFiles);
343 		config.setAssertJsonSize(this.assertJsonSize);
344 		config.setPackageWithBaseClasses(this.packageWithBaseClasses);
345 		if (this.baseClassMappings != null) {
346 			config.setBaseClassMappings(mappingsToMap());
347 		}
348 	}
349 
350 	public Map<String, String> mappingsToMap() {
351 		Map<String, String> map = new HashMap<>();
352 		if (this.baseClassMappings == null) {
353 			return map;
354 		}
355 		for (BaseClassMapping mapping : this.baseClassMappings) {
356 			map.put(mapping.getContractPackageRegex(), mapping.getBaseClassFQN());
357 		}
358 		return map;
359 	}
360 
361 	public List<String> getExcludedFiles() {
362 		return this.excludedFiles;
363 	}
364 
365 	public void setExcludedFiles(List<String> excludedFiles) {
366 		this.excludedFiles = excludedFiles;
367 	}
368 
369 	public List<String> getIgnoredFiles() {
370 		return this.ignoredFiles;
371 	}
372 
373 	public void setIgnoredFiles(List<String> ignoredFiles) {
374 		this.ignoredFiles = ignoredFiles;
375 	}
376 
377 	public boolean isAssertJsonSize() {
378 		return this.assertJsonSize;
379 	}
380 
381 	public void setAssertJsonSize(boolean assertJsonSize) {
382 		this.assertJsonSize = assertJsonSize;
383 	}
384 
385 }