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