NO_OP_ACTION = c -> {};
53 |
54 | private final Project project;
55 |
56 | @Inject
57 | public JavaModuleTestingExtension(Project project) {
58 | this.project = project;
59 |
60 | TestingExtension testing = project.getExtensions().getByType(TestingExtension.class);
61 | testing.getSuites().withType(JvmTestSuite.class).configureEach(jvmTestSuite -> {
62 | boolean isTestModule = jvmTestSuite.getSources().getJava().getSrcDirs().stream().anyMatch(src -> new File(src, "module-info.java").exists());
63 | if ("test".equals(jvmTestSuite.getName())) {
64 | jvmTestSuite.useJUnitJupiter(); // override old Gradle convention to default to JUnit5 for all suites
65 | }
66 |
67 | if (isTestModule) {
68 | blackbox(jvmTestSuite);
69 | } else {
70 | whitebox(jvmTestSuite, conf -> conf.getOpensTo().add("org.junit.platform.commons"));
71 | }
72 | });
73 | }
74 |
75 | /**
76 | * Turn the given JVM Test Suite into a Blackbox Test Suite.
77 | * For example:
78 | *
79 | * javaModuleTesting.blackbox(testing.suites["integtest"])
80 | *
81 | * @param jvmTestSuite the JVM Test Suite to configure
82 | */
83 | public void blackbox(TestSuite jvmTestSuite) {
84 | if (jvmTestSuite instanceof JvmTestSuite) {
85 | configureJvmTestSuiteForBlackbox((JvmTestSuite) jvmTestSuite);
86 | }
87 | }
88 |
89 | /**
90 | * Turn the given JVM Test Suite into a Whitebox Test Suite.
91 | * For example:
92 | *
93 | * javaModuleTesting.whitebox(testing.suites["test"])
94 | *
95 | * @param jvmTestSuite the JVM Test Suite to configure
96 | */
97 | @SuppressWarnings("unused")
98 | public void whitebox(TestSuite jvmTestSuite) {
99 | whitebox(jvmTestSuite, NO_OP_ACTION);
100 | }
101 |
102 | /**
103 | * Turn the given JVM Test Suite into a Classpath Test Suite.
104 | * For example:
105 | *
106 | * javaModuleTesting.classpath(testing.suites["test"])
107 | *
108 | * This restores the default behavior of Gradle to run tests on the Classpath if
109 | * no 'module-info.java' is present in the source folder of the given test suite.
110 | *
111 | * @param jvmTestSuite the JVM Test Suite to configure
112 | */
113 | @SuppressWarnings("unused")
114 | public void classpath(TestSuite jvmTestSuite) {
115 | if (jvmTestSuite instanceof JvmTestSuite) {
116 | revertJvmTestSuiteForWhitebox((JvmTestSuite) jvmTestSuite);
117 | }
118 | }
119 |
120 | /**
121 | * Turn the given JVM Test Suite into a Whitebox Test Suite.
122 | * If needed, configure additional 'requires' and open the
123 | * test packages for reflection.
124 | *
125 | * For example, for JUnit 5, you need at least:
126 | *
127 | * javaModuleTesting.whitebox(testing.suites["test"]) {
128 | * requires.add("org.junit.jupiter.api")
129 | * opensTo.add("org.junit.platform.commons")
130 | * }
131 | *
132 | * @param jvmTestSuite the JVM Test Suite to configure
133 | * @param conf configuration details for the whitebox test setup
134 | */
135 | public void whitebox(TestSuite jvmTestSuite, Action conf) {
136 | if (jvmTestSuite instanceof JvmTestSuite) {
137 | SourceSet suiteSourceSet = ((JvmTestSuite) jvmTestSuite).getSources();
138 | boolean testFolderExists = suiteSourceSet.getJava().getSrcDirs().stream().anyMatch(File::exists);
139 | if (!testFolderExists) {
140 | // Remove the dependencies added by Gradle in case the test directory is missing. Then stop. This allows the use of 'useJUnitJupiter("")' without hassle.
141 | project.getConfigurations().getByName(suiteSourceSet.getImplementationConfigurationName(), implementation ->
142 | implementation.withDependencies(dependencySet -> dependencySet.removeIf(d -> "org.junit.jupiter".equals(d.getGroup()) && "junit-jupiter".equals(d.getName()))));
143 | project.getConfigurations().getByName(suiteSourceSet.getRuntimeOnlyConfigurationName(), runtimeOnly ->
144 | runtimeOnly.withDependencies(dependencySet -> dependencySet.removeIf(d -> "org.junit.platform".equals(d.getGroup()) && "junit-platform-launcher".equals(d.getName()))));
145 | return;
146 | }
147 |
148 | WhiteboxJvmTestSuite whiteboxJvmTestSuite = project.getObjects().newInstance(WhiteboxJvmTestSuite.class);
149 | whiteboxJvmTestSuite.getSourcesUnderTest().convention(project.getExtensions().getByType(SourceSetContainer.class).getByName(SourceSet.MAIN_SOURCE_SET_NAME));
150 | whiteboxJvmTestSuite.getRequires().addAll(requiresFromModuleInfo((JvmTestSuite) jvmTestSuite, whiteboxJvmTestSuite.getSourcesUnderTest(), false));
151 | whiteboxJvmTestSuite.getRequiresRuntime().addAll(requiresFromModuleInfo((JvmTestSuite) jvmTestSuite, whiteboxJvmTestSuite.getSourcesUnderTest(), true));
152 | conf.execute(whiteboxJvmTestSuite);
153 | configureJvmTestSuiteForWhitebox((JvmTestSuite) jvmTestSuite, whiteboxJvmTestSuite);
154 | }
155 | }
156 |
157 | private Provider> requiresFromModuleInfo(JvmTestSuite jvmTestSuite, Provider sourcesUnderTest, boolean runtimeOnly) {
158 | RegularFile moduleInfoFile = project.getLayout().getProjectDirectory().file(whiteboxModuleInfo(jvmTestSuite).getAbsolutePath());
159 | Provider moduleInfoContent = project.getProviders().fileContents(moduleInfoFile).getAsText();
160 | return moduleInfoContent.map(c -> {
161 | ModuleInfoParser moduleInfoParser = new ModuleInfoParser(project.getLayout(), project.getProviders());
162 | String mainModuleName = moduleInfoParser.moduleName(sourcesUnderTest.get().getAllJava().getSrcDirs());
163 | List requires = ModuleInfoRequiresParser.parse(moduleInfoContent.get(), runtimeOnly);
164 | if (requires.stream().anyMatch(r -> r.equals(mainModuleName)) || runtimeOnly) {
165 | return requires.stream().filter(r -> !r.equals(mainModuleName)).collect(Collectors.toList());
166 | }
167 | return Collections.emptyList();
168 | }).orElse(Collections.emptyList());
169 | }
170 |
171 | private File whiteboxModuleInfo(JvmTestSuite jvmTestSuite) {
172 | File sourceSetDir = jvmTestSuite.getSources().getJava().getSrcDirs().iterator().next().getParentFile();
173 | return new File(sourceSetDir, "java9/module-info.java");
174 | }
175 |
176 | private void configureJvmTestSuiteForBlackbox(JvmTestSuite jvmTestSuite) {
177 | ConfigurationContainer configurations = project.getConfigurations();
178 | TaskContainer tasks = project.getTasks();
179 |
180 | TaskProvider jarTask;
181 | SourceSet sourceSet = jvmTestSuite.getSources();
182 | if (!tasks.getNames().contains(sourceSet.getJarTaskName())) {
183 | jarTask = tasks.register(sourceSet.getJarTaskName(), Jar.class, t -> {
184 | t.getArchiveClassifier().set(sourceSet.getName());
185 | t.from(sourceSet.getOutput());
186 | });
187 | } else {
188 | jarTask = tasks.named(sourceSet.getJarTaskName(), Jar.class);
189 | }
190 |
191 | tasks.named(sourceSet.getName(), Test.class, t -> {
192 | // Classpath consists only of Jars to include classes+resources in one place
193 | t.setClasspath(configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()).plus(project.files(jarTask)));
194 | // Reset test classes dir
195 | t.setTestClassesDirs(sourceSet.getOutput().getClassesDirs());
196 | });
197 | }
198 |
199 | private void configureJvmTestSuiteForWhitebox(JvmTestSuite jvmTestSuite, WhiteboxJvmTestSuite whiteboxJvmTestSuite) {
200 | ConfigurationContainer configurations = project.getConfigurations();
201 | DependencyHandler dependencies = project.getDependencies();
202 | TaskContainer tasks = project.getTasks();
203 | ModuleInfoParser moduleInfoParser = new ModuleInfoParser(project.getLayout(), project.getProviders());
204 |
205 | SourceSet testSources = jvmTestSuite.getSources();
206 | JavaModuleDependenciesBridge.addRequiresRuntimeSupport(project, whiteboxJvmTestSuite.getSourcesUnderTest().get(), jvmTestSuite.getSources());
207 |
208 | tasks.named(testSources.getCompileJavaTaskName(), JavaCompile.class, compileJava -> {
209 | SourceSet sourcesUnderTest = whiteboxJvmTestSuite.getSourcesUnderTest().get();
210 |
211 | Configuration compileOnly = configurations.getByName(sourcesUnderTest.getCompileOnlyConfigurationName());
212 | Configuration testCompileOnly = configurations.getByName(testSources.getCompileOnlyConfigurationName());
213 | if (!testCompileOnly.getExtendsFrom().contains(compileOnly)) {
214 | testCompileOnly.extendsFrom(compileOnly);
215 | }
216 |
217 | compileJava.setClasspath(sourcesUnderTest.getOutput().plus(configurations.getByName(testSources.getCompileClasspathConfigurationName())));
218 |
219 | WhiteboxTestCompileArgumentProvider argumentProvider = (WhiteboxTestCompileArgumentProvider) compileJava.getOptions().getCompilerArgumentProviders().stream()
220 | .filter(p -> p instanceof WhiteboxTestCompileArgumentProvider).findFirst().orElseGet(() -> {
221 | WhiteboxTestCompileArgumentProvider newProvider = new WhiteboxTestCompileArgumentProvider(
222 | sourcesUnderTest.getJava().getSrcDirs(),
223 | testSources.getJava().getSrcDirs(),
224 | moduleInfoParser,
225 | project.getObjects());
226 | compileJava.getOptions().getCompilerArgumentProviders().add(newProvider);
227 | compileJava.doFirst(project.getObjects().newInstance(JavaCompileSetModulePathAction.class));
228 | return newProvider;
229 | });
230 | argumentProvider.testRequires(JavaModuleDependenciesBridge.getCompileClasspathModules(project, testSources));
231 | argumentProvider.testRequires(whiteboxJvmTestSuite.getRequires());
232 | });
233 |
234 | tasks.named(testSources.getName(), Test.class, test -> {
235 | SourceSet sourcesUnderTest = whiteboxJvmTestSuite.getSourcesUnderTest().get();
236 | test.setClasspath(configurations.getByName(testSources.getRuntimeClasspathConfigurationName()).plus(sourcesUnderTest.getOutput()).plus(testSources.getOutput()));
237 |
238 | // Add main classes here so that Gradle finds module-info.class and treats this as a test with module path
239 | test.setTestClassesDirs(sourcesUnderTest.getOutput().getClassesDirs().plus(testSources.getOutput().getClassesDirs()));
240 |
241 | WhiteboxTestRuntimeArgumentProvider argumentProvider = (WhiteboxTestRuntimeArgumentProvider) test.getJvmArgumentProviders().stream()
242 | .filter(p -> p instanceof WhiteboxTestRuntimeArgumentProvider).findFirst().orElseGet(() -> {
243 | WhiteboxTestRuntimeArgumentProvider newProvider = new WhiteboxTestRuntimeArgumentProvider(
244 | sourcesUnderTest.getJava().getSrcDirs(),
245 | testSources.getJava().getClassesDirectory(),
246 | sourcesUnderTest.getOutput().getResourcesDir(),
247 | testSources.getOutput().getResourcesDir(),
248 | moduleInfoParser,
249 | project.getObjects());
250 | test.getJvmArgumentProviders().add(newProvider);
251 | return newProvider;
252 | });
253 | argumentProvider.testRequires(JavaModuleDependenciesBridge.getRuntimeClasspathModules(project, testSources));
254 | argumentProvider.testRequires(whiteboxJvmTestSuite.getRequires());
255 | argumentProvider.testOpensTo(JavaModuleDependenciesBridge.getOpensToModules(project, testSources));
256 | argumentProvider.testOpensTo(whiteboxJvmTestSuite.getOpensTo());
257 | argumentProvider.testExportsTo(JavaModuleDependenciesBridge.getExportsToModules(project, testSources));
258 | argumentProvider.testExportsTo(whiteboxJvmTestSuite.getExportsTo());
259 | });
260 |
261 | Configuration implementation = configurations.getByName(testSources.getImplementationConfigurationName());
262 | implementation.withDependencies(d -> {
263 | for (String requiresModuleName : whiteboxJvmTestSuite.getRequires().get()) {
264 | Provider> dependency = JavaModuleDependenciesBridge.create(project, requiresModuleName, whiteboxJvmTestSuite.getSourcesUnderTest().get());
265 | if (dependency != null) {
266 | dependencies.addProvider(implementation.getName(), dependency);
267 | }
268 | }
269 | });
270 | Configuration runtimeOnly = configurations.getByName(testSources.getRuntimeOnlyConfigurationName());
271 | runtimeOnly.withDependencies(d -> {
272 | for (String requiresModuleName : whiteboxJvmTestSuite.getRequiresRuntime().get()) {
273 | Provider> dependency = JavaModuleDependenciesBridge.create(project, requiresModuleName, whiteboxJvmTestSuite.getSourcesUnderTest().get());
274 | if (dependency != null) {
275 | dependencies.addProvider(runtimeOnly.getName(), dependency);
276 | }
277 | }
278 | });
279 | }
280 |
281 | /**
282 | * Resets changes performed in 'configureJvmTestSuiteForWhitebox' to Gradle defaults.
283 | */
284 | private void revertJvmTestSuiteForWhitebox(JvmTestSuite jvmTestSuite) {
285 | TaskContainer tasks = project.getTasks();
286 | SourceSet testSources = jvmTestSuite.getSources();
287 |
288 | tasks.named(testSources.getCompileJavaTaskName(), JavaCompile.class, compileJava -> {
289 | compileJava.setClasspath(testSources.getCompileClasspath());
290 | compileJava.getOptions().getCompilerArgumentProviders().removeIf(p -> p instanceof WhiteboxTestCompileArgumentProvider);
291 | compileJava.getActions().removeIf(a -> a instanceof Describable
292 | && JavaCompileSetModulePathAction.class.getName().equals(((Describable) a).getDisplayName()));
293 | });
294 |
295 | tasks.named(testSources.getName(), Test.class, test -> {
296 | test.setClasspath(testSources.getRuntimeClasspath());
297 | test.setTestClassesDirs(testSources.getOutput().getClassesDirs());
298 | test.getJvmArgumentProviders().removeIf(p -> p instanceof WhiteboxTestRuntimeArgumentProvider);
299 | });
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/JavaModuleTestingPlugin.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing;
18 |
19 | import org.gradle.api.NonNullApi;
20 | import org.gradle.api.Plugin;
21 | import org.gradle.api.Project;
22 | import org.gradle.api.plugins.JvmTestSuitePlugin;
23 | import org.gradle.util.GradleVersion;
24 |
25 | @SuppressWarnings("unused")
26 | @NonNullApi
27 | public abstract class JavaModuleTestingPlugin implements Plugin {
28 |
29 | @Override
30 | public void apply(Project project) {
31 | if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) < 0) {
32 | throw new RuntimeException("This plugin requires Gradle 7.4+");
33 | }
34 | project.getPlugins().withType(JvmTestSuitePlugin.class, p->
35 | project.getExtensions().create("javaModuleTesting", JavaModuleTestingExtension.class));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/TaskLockService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing;
18 |
19 | import org.gradle.api.services.BuildService;
20 | import org.gradle.api.services.BuildServiceParameters;
21 |
22 | /**
23 | * No-op service that can serve as 'lock' to prevent multiple test tasks from running in parallel:
24 | * usesService(gradle.sharedServices.registerIfAbsent(TaskLockService.NAME, TaskLockService::class) { maxParallelUsages = 1 })
25 | */
26 | public abstract class TaskLockService implements BuildService {
27 | public static String NAME = "taskLock";
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/WhiteboxJvmTestSuite.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing;
18 |
19 | import org.gradle.api.provider.ListProperty;
20 | import org.gradle.api.provider.Property;
21 | import org.gradle.api.tasks.SourceSet;
22 |
23 | public interface WhiteboxJvmTestSuite {
24 |
25 | /**
26 | * Configure which source set contains the 'sources under test' for
27 | * this Whitebox Test Suite - defaults to 'main'.
28 | *
29 | * @return the source set under test
30 | */
31 | Property getSourcesUnderTest();
32 |
33 | /**
34 | * Add additional 'requires' directives for the test code.
35 | * For example, 'requires.add("org.junit.jupiter.api")'.
36 | *
37 | * @return modifiable list of addition 'requires' (--add-reads)
38 | */
39 | ListProperty getRequires();
40 |
41 | /**
42 | * Add a runtime-only dependency via Module Name when combined with
43 | * 'java-module-dependencies' plugin.
44 | *
45 | * @return modifiable list of addition 'runtimeOnly' dependencies
46 | */
47 | ListProperty getRequiresRuntime();
48 |
49 | /**
50 | * Open all packages of this Whitebox Test Suite to a given Module
51 | * for reflection at runtime.
52 | * For example, 'opensTo.add("org.junit.platform.commons")'.
53 | *
54 | * @return modifiable list of addition '--add-opens'
55 | */
56 | ListProperty getOpensTo();
57 |
58 | /**
59 | * Export all packages of this Whitebox Test Suite to a given Module
60 | * for access to public methods at runtime.
61 | *
62 | * @return modifiable list of addition '--add-exports'
63 | */
64 | ListProperty getExportsTo();
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/ModuleInfoParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal;
18 |
19 | import org.gradle.api.file.ProjectLayout;
20 | import org.gradle.api.file.RegularFile;
21 | import org.gradle.api.provider.Provider;
22 | import org.gradle.api.provider.ProviderFactory;
23 |
24 | import java.io.File;
25 | import java.util.Arrays;
26 | import java.util.List;
27 | import java.util.Set;
28 |
29 | public class ModuleInfoParser {
30 |
31 | private final ProjectLayout layout;
32 | private final ProviderFactory providers;
33 |
34 | public ModuleInfoParser(ProjectLayout layout, ProviderFactory providers) {
35 | this.layout = layout;
36 | this.providers = providers;
37 | }
38 |
39 | public String moduleName(Set sourceFolders) {
40 | for (File folder : sourceFolders) {
41 | Provider moduleInfoFile = layout.file(providers.provider(() -> new File(folder, "module-info.java")));
42 | Provider moduleInfoContent = providers.fileContents(moduleInfoFile).getAsText();
43 | if (moduleInfoContent.isPresent()) {
44 | return moduleName(moduleInfoContent.get());
45 | }
46 | }
47 | return null;
48 | }
49 |
50 | static String moduleName(String moduleInfoFileContent) {
51 | boolean inComment = false;
52 | boolean moduleKeywordFound = false;
53 |
54 | for(String line: moduleInfoFileContent.split("\n")) {
55 | String cleanedLine = line
56 | .replaceAll("/\\*.*\\*/", "") // open & close in this line
57 | .replaceAll("//.*", ""); // line comment
58 | inComment = inComment || cleanedLine.contains("/*");
59 | cleanedLine = cleanedLine.replaceAll("/\\*.*", ""); // open in this line
60 | inComment = inComment && !line.contains("*/");
61 | cleanedLine = cleanedLine.replaceAll(".*\\*/", "").trim(); // closing part of comment
62 |
63 | if (inComment) {
64 | continue;
65 | }
66 |
67 | List tokens = Arrays.asList(cleanedLine.split("\\s+"));
68 | if (moduleKeywordFound && !tokens.isEmpty()) {
69 | return tokens.get(0);
70 | }
71 |
72 | int moduleKeywordIndex = tokens.indexOf("module");
73 | if (moduleKeywordIndex == 0 || moduleKeywordIndex == 1) {
74 | if (tokens.size() > moduleKeywordIndex) {
75 | return tokens.get(moduleKeywordIndex + 1);
76 | }
77 | moduleKeywordFound = true;
78 | }
79 | }
80 | return null;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/ModuleInfoRequiresParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.List;
22 |
23 | public class ModuleInfoRequiresParser {
24 | private static final String RUNTIME_KEYWORD = "/*runtime*/";
25 |
26 | public static List parse(String moduleInfoFileContent, boolean runtimeOnly) {
27 | List requires = new ArrayList<>();
28 | boolean insideComment = false;
29 | for(String line: moduleInfoFileContent.split("\n")) {
30 | insideComment = parseLine(line, insideComment, requires, runtimeOnly);
31 | }
32 | return requires;
33 | }
34 |
35 | /**
36 | * @return true, if we are inside a multi-line comment after this line
37 | */
38 | private static boolean parseLine(String moduleLine, boolean insideComment, List requires, boolean runtimeOnly) {
39 | if (insideComment) {
40 | return !moduleLine.contains("*/");
41 | }
42 |
43 | List tokens = Arrays.asList(moduleLine
44 | .replace(";", "")
45 | .replace("{", "")
46 | .replace(RUNTIME_KEYWORD, "runtime")
47 | .replaceAll("/\\*.*?\\*/", " ")
48 | .trim().split("\\s+"));
49 | int singleLineCommentStartIndex = tokens.indexOf("//");
50 | if (singleLineCommentStartIndex >= 0) {
51 | tokens = tokens.subList(0, singleLineCommentStartIndex);
52 | }
53 |
54 | if (tokens.size() > 1 && tokens.get(0).equals("requires")) {
55 | if (runtimeOnly) {
56 | if (tokens.size() > 2 && tokens.contains("runtime")) {
57 | requires.add(tokens.get(2));
58 | }
59 | } else {
60 | if (tokens.size() > 3 && tokens.contains("static") && tokens.contains("transitive")) {
61 | requires.add(tokens.get(3));
62 | } else if (tokens.size() > 2 && tokens.contains("transitive")) {
63 | requires.add(tokens.get(2));
64 | } else if (tokens.size() > 2 && tokens.contains("static")) {
65 | requires.add(tokens.get(2));
66 | } else if (!tokens.contains("runtime")) {
67 | requires.add(tokens.get(1));
68 | }
69 | }
70 | }
71 | return moduleLine.lastIndexOf("/*") > moduleLine.lastIndexOf("*/");
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/actions/JavaCompileSetModulePathAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal.actions;
18 |
19 | import org.gradle.api.Action;
20 | import org.gradle.api.Describable;
21 | import org.gradle.api.NonNullApi;
22 | import org.gradle.api.Task;
23 | import org.gradle.api.file.FileCollection;
24 | import org.gradle.api.tasks.compile.JavaCompile;
25 | import org.gradle.internal.jvm.JavaModuleDetector;
26 |
27 | import javax.inject.Inject;
28 | import java.util.ArrayList;
29 | import java.util.List;
30 |
31 | @NonNullApi
32 | public abstract class JavaCompileSetModulePathAction implements Action, Describable {
33 |
34 | @Inject
35 | protected abstract JavaModuleDetector getJavaModuleDetector();
36 |
37 | @Override
38 | public String getDisplayName() {
39 | return JavaCompileSetModulePathAction.class.getName();
40 | }
41 |
42 | @Override
43 | public void execute(Task task) {
44 | JavaCompile javaCompile = (JavaCompile) task;
45 | FileCollection classpathAndModulePath = javaCompile.getClasspath();
46 | List compilerArgs = new ArrayList<>(javaCompile.getOptions().getCompilerArgs());
47 |
48 | // Since for Gradle this sources set does not look like a module, we have to define the module path ourselves
49 | compilerArgs.add("--module-path");
50 | compilerArgs.add(getJavaModuleDetector().inferModulePath(true, classpathAndModulePath).getAsPath());
51 | javaCompile.setClasspath(getJavaModuleDetector().inferClasspath(true, classpathAndModulePath));
52 | javaCompile.getOptions().setCompilerArgs(compilerArgs);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/bridges/JavaModuleDependenciesBridge.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal.bridges;
18 |
19 | import org.gradle.api.Project;
20 | import org.gradle.api.provider.Provider;
21 | import org.gradle.api.tasks.SourceSet;
22 |
23 | import java.lang.reflect.Method;
24 | import java.util.Collections;
25 | import java.util.List;
26 |
27 | public class JavaModuleDependenciesBridge {
28 |
29 | public static Provider> create(Project project, String moduleName, SourceSet sourceSetWithModuleInfo) {
30 | Object javaModuleDependencies = project.getExtensions().findByName("javaModuleDependencies");
31 | if (javaModuleDependencies == null) {
32 | return null;
33 | }
34 | try {
35 | Method gav = javaModuleDependencies.getClass().getMethod("create", String.class, SourceSet.class);
36 | return (Provider>) gav.invoke(javaModuleDependencies, moduleName, sourceSetWithModuleInfo);
37 | } catch (ReflectiveOperationException e) {
38 | throw new RuntimeException(e);
39 | }
40 | }
41 |
42 | public static void addRequiresRuntimeSupport(Project project, SourceSet sourceSetForModuleInfo, SourceSet sourceSetForClasspath) {
43 | Object javaModuleDependencies = project.getExtensions().findByName("javaModuleDependencies");
44 | if (javaModuleDependencies == null) {
45 | return;
46 | }
47 | try {
48 | Method addRequiresRuntimeSupport = javaModuleDependencies.getClass().getMethod("addRequiresRuntimeSupport", SourceSet.class, SourceSet.class);
49 | addRequiresRuntimeSupport.invoke(javaModuleDependencies, sourceSetForModuleInfo, sourceSetForClasspath);
50 | } catch (NoSuchMethodException e) {
51 | //noinspection UnnecessaryReturnStatement
52 | return;
53 | } catch (ReflectiveOperationException e) {
54 | throw new RuntimeException(e);
55 | }
56 | }
57 |
58 | public static List getRuntimeClasspathModules(Project project, SourceSet sourceSet) {
59 | return getDeclaredModules("getRuntimeClasspathModules", project, sourceSet);
60 | }
61 |
62 | public static List getCompileClasspathModules(Project project, SourceSet sourceSet) {
63 | return getDeclaredModules("getCompileClasspathModules", project, sourceSet);
64 | }
65 |
66 | public static List getExportsToModules(Project project, SourceSet sourceSet) {
67 | return getDeclaredModules("getExportsToModules", project, sourceSet);
68 | }
69 |
70 | public static List getOpensToModules(Project project, SourceSet sourceSet) {
71 | return getDeclaredModules("getOpensToModules", project, sourceSet);
72 | }
73 |
74 | private static List getDeclaredModules(String getter, Project project, SourceSet sourceSet) {
75 | Object moduleInfoDslExtension = project.getExtensions().findByName(sourceSet.getName() + "ModuleInfo");
76 | if (moduleInfoDslExtension == null) {
77 | return Collections.emptyList();
78 | }
79 | try {
80 | Method gav = moduleInfoDslExtension.getClass().getMethod(getter);
81 | @SuppressWarnings("unchecked")
82 | List modules = (List) gav.invoke(moduleInfoDslExtension);
83 | return modules;
84 | } catch (NoSuchMethodException e) {
85 | return Collections.emptyList();
86 | } catch (ReflectiveOperationException e) {
87 | throw new RuntimeException(e);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/provider/WhiteboxTestCompileArgumentProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal.provider;
18 |
19 | import org.gradle.api.file.FileCollection;
20 | import org.gradle.api.model.ObjectFactory;
21 | import org.gradle.api.provider.ListProperty;
22 | import org.gradle.api.provider.Provider;
23 | import org.gradlex.javamodule.testing.internal.ModuleInfoParser;
24 | import org.gradle.api.tasks.compile.JavaCompile;
25 | import org.gradle.internal.jvm.JavaModuleDetector;
26 | import org.gradle.process.CommandLineArgumentProvider;
27 |
28 | import java.io.File;
29 | import java.util.ArrayList;
30 | import java.util.List;
31 | import java.util.Set;
32 | import java.util.stream.Collectors;
33 |
34 | public class WhiteboxTestCompileArgumentProvider implements CommandLineArgumentProvider {
35 | private final Set mainSourceFolders;
36 | private final Set testSourceFolders;
37 | private final ModuleInfoParser moduleInfoParser;
38 |
39 | private final ListProperty allTestRequires;
40 |
41 | public WhiteboxTestCompileArgumentProvider(
42 | Set mainSourceFolders, Set testSourceFolders, ModuleInfoParser moduleInfoParser, ObjectFactory objects) {
43 | this.mainSourceFolders = mainSourceFolders;
44 | this.testSourceFolders = testSourceFolders;
45 | this.moduleInfoParser = moduleInfoParser;
46 | this.allTestRequires = objects.listProperty(String.class);
47 | }
48 |
49 | public void testRequires(Provider> testRequires) {
50 | allTestRequires.addAll(testRequires);
51 | }
52 |
53 | public void testRequires(List testRequires) {
54 | allTestRequires.addAll(testRequires);
55 | }
56 |
57 | @Override
58 | public Iterable asArguments() {
59 | String moduleName = moduleInfoParser.moduleName(mainSourceFolders);
60 | String testSources = testSourceFolders.stream().map(File::getPath)
61 | .collect(Collectors.joining(File.pathSeparator));
62 |
63 | List args = new ArrayList<>();
64 |
65 | for (String testRequires : allTestRequires.get()) {
66 | args.add("--add-modules");
67 | args.add(testRequires);
68 | args.add("--add-reads");
69 | args.add(moduleName + "=" + testRequires);
70 | }
71 |
72 | // Patch 'main' and 'test' sources together
73 | args.add("--patch-module");
74 | args.add(moduleName + "=" + testSources);
75 |
76 | return args;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/org/gradlex/javamodule/testing/internal/provider/WhiteboxTestRuntimeArgumentProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.internal.provider;
18 |
19 | import org.gradle.api.file.Directory;
20 | import org.gradle.api.model.ObjectFactory;
21 | import org.gradle.api.provider.ListProperty;
22 | import org.gradle.api.provider.Provider;
23 | import org.gradle.process.CommandLineArgumentProvider;
24 | import org.gradlex.javamodule.testing.internal.ModuleInfoParser;
25 |
26 | import java.io.File;
27 | import java.util.ArrayList;
28 | import java.util.List;
29 | import java.util.Set;
30 | import java.util.TreeSet;
31 |
32 | public class WhiteboxTestRuntimeArgumentProvider implements CommandLineArgumentProvider {
33 | private final Set mainSourceFolders;
34 | private final Provider testClassesFolders;
35 | private final File resourcesUnderTest;
36 | private final File testResources;
37 | private final ModuleInfoParser moduleInfoParser;
38 |
39 | private final ListProperty allTestRequires;
40 | private final ListProperty allTestOpensTo;
41 | private final ListProperty allTestExportsTo;
42 |
43 | public WhiteboxTestRuntimeArgumentProvider(Set mainSourceFolders,
44 | Provider testClassesFolders, File resourcesUnderTest, File testResources,
45 | ModuleInfoParser moduleInfoParser, ObjectFactory objects) {
46 |
47 | this.mainSourceFolders = mainSourceFolders;
48 | this.testClassesFolders = testClassesFolders;
49 | this.resourcesUnderTest = resourcesUnderTest;
50 | this.testResources = testResources;
51 | this.moduleInfoParser = moduleInfoParser;
52 | this.allTestRequires = objects.listProperty(String.class);
53 | this.allTestOpensTo = objects.listProperty(String.class);
54 | this.allTestExportsTo = objects.listProperty(String.class);
55 | }
56 |
57 | public void testRequires(Provider> testRequires) {
58 | allTestRequires.addAll(testRequires);
59 | }
60 |
61 | public void testRequires(List testRequires) {
62 | allTestRequires.addAll(testRequires);
63 | }
64 |
65 | public void testOpensTo(Provider> testOpensTo) {
66 | allTestOpensTo.addAll(testOpensTo);
67 | }
68 |
69 | public void testOpensTo(List testOpensTo) {
70 | allTestOpensTo.addAll(testOpensTo);
71 | }
72 |
73 | public void testExportsTo(Provider> testExportsTo) {
74 | allTestExportsTo.addAll(testExportsTo);
75 | }
76 |
77 | public void testExportsTo(List testExportsTo) {
78 | allTestExportsTo.addAll(testExportsTo);
79 | }
80 |
81 | @Override
82 | public Iterable asArguments() {
83 | String moduleName = moduleInfoParser.moduleName(mainSourceFolders);
84 |
85 | Set allTestClassPackages = new TreeSet<>();
86 | testClassesFolders.get().getAsFileTree().visit(file -> {
87 | String path = file.getPath();
88 | if (path.endsWith(".class") && path.contains("/")) {
89 | allTestClassPackages.add(path.substring(0, path.lastIndexOf("/")).replace('/', '.'));
90 | }
91 | });
92 |
93 | List args = new ArrayList<>();
94 |
95 | for (String testRequires : allTestRequires.get()) {
96 | args.add("--add-modules");
97 | args.add(testRequires);
98 | args.add("--add-reads");
99 | args.add(moduleName + "=" + testRequires);
100 | }
101 |
102 | for (String packageName : allTestClassPackages) {
103 | for (String opensTo : allTestOpensTo.get()) {
104 | args.add("--add-opens");
105 | args.add(moduleName + "/" + packageName + "=" + opensTo);
106 | }
107 | }
108 |
109 | for (String packageName : allTestClassPackages) {
110 | for (String opensTo : allTestExportsTo.get()) {
111 | args.add("--add-exports");
112 | args.add(moduleName + "/" + packageName + "=" + opensTo);
113 | }
114 | }
115 |
116 | String testClassesPath = testClassesFolders.get().getAsFile().getPath();
117 | String resourcesUnderTestPath = toAppendablePathEntry(resourcesUnderTest);
118 | String testResourcesPath = toAppendablePathEntry(testResources);
119 |
120 | // Patch into Module located in the 'main' classes folder: test classes, resources, test resources
121 | args.add("--patch-module");
122 | args.add(moduleName + "=" + testClassesPath + resourcesUnderTestPath + testResourcesPath);
123 |
124 | return args;
125 | }
126 |
127 | private String toAppendablePathEntry(File folder) {
128 | if (folder.exists()) {
129 | return File.pathSeparator + folder.getPath();
130 | } else {
131 | return "";
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/internal/ModuleInfoParseTest.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.internal
2 |
3 | import spock.lang.Specification
4 |
5 | class ModuleInfoParseTest extends Specification {
6 |
7 | def "ignores single line comments"() {
8 | given:
9 | def nameFromFile = ModuleInfoParser.moduleName('''
10 | // module some.thing.else
11 | module some.thing {
12 | requires transitive foo.bar.la;
13 | }
14 | ''')
15 |
16 | expect:
17 | nameFromFile == 'some.thing'
18 | }
19 |
20 | def "ignores single line comments late in line"() {
21 | given:
22 | def nameFromFile = ModuleInfoParser.moduleName('''
23 | module some.thing { // module some.thing.else
24 | requires transitive foo.bar.la;
25 | }
26 | ''')
27 |
28 | expect:
29 | nameFromFile == 'some.thing'
30 | }
31 |
32 | def "ignores multi line comments"() {
33 | given:
34 | def nameFromFile = ModuleInfoParser.moduleName('''
35 | /*
36 | module some.thing.else;
37 | */
38 | module some.thing {
39 | requires static foo.bar.la;
40 | }
41 | ''')
42 |
43 | expect:
44 | nameFromFile == 'some.thing'
45 | }
46 |
47 | def "ignores multi line comments between keywords"() {
48 | given:
49 | def nameFromFile = ModuleInfoParser.moduleName('''
50 | module /*module some.other*/ some.thing { /* module
51 | odd comment*/ requires transitive foo.bar.la;
52 | requires/* weird comment*/ static foo.bar.lo;
53 | requires /*something to say*/foo.bar.li; /*
54 | requires only.a.comment
55 | */
56 | }
57 | ''')
58 |
59 | expect:
60 | nameFromFile == 'some.thing'
61 | }
62 |
63 | def "finds module name when open keyword is used"() {
64 | given:
65 | def nameFromFile = ModuleInfoParser.moduleName('''
66 | open module /*module some.other*/ some.thing { /* module
67 | odd comment*/ requires transitive foo.bar.la;
68 | requires/* weird comment*/ static foo.bar.lo;
69 | requires /*something to say*/foo.bar.li; /*
70 | requires only.a.comment
71 | */
72 | }
73 | ''')
74 |
75 | expect:
76 | nameFromFile == 'some.thing'
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/test/ClasspathSuiteTest.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.test
2 |
3 | import org.gradle.testkit.runner.TaskOutcome
4 | import org.gradlex.javamodule.testing.test.fixture.GradleBuild
5 | import spock.lang.Specification
6 |
7 | class ClasspathSuiteTest extends Specification {
8 |
9 | @Delegate
10 | GradleBuild build = new GradleBuild()
11 |
12 | def "can configure classpath test suite"() {
13 | given:
14 | appBuildFile << '''
15 | javaModuleTesting.classpath(testing.suites["test"])
16 | '''
17 | appModuleInfoFile << '''
18 | module org.example.app {
19 | }
20 | '''
21 |
22 | when:
23 | def result = runTests()
24 |
25 | then:
26 | result.output.contains('Main Module: null')
27 | result.output.contains('Test Module: null')
28 | result.task(':app:test').outcome == TaskOutcome.SUCCESS
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/test/CoreFunctionailtyTest.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.test
2 |
3 | import org.gradle.testkit.runner.TaskOutcome
4 | import org.gradlex.javamodule.testing.test.fixture.GradleBuild
5 | import spock.lang.Specification
6 |
7 | class CoreFunctionailtyTest extends Specification {
8 |
9 | @Delegate
10 | GradleBuild build = new GradleBuild()
11 |
12 | def "testCompileOnly extends compileOnly for whitebox test suites"() {
13 | given:
14 | appBuildFile << '''
15 | javaModuleTesting.classpath(testing.suites["test"])
16 | dependencies {
17 | compileOnly("jakarta.servlet:jakarta.servlet-api:6.1.0")
18 | }
19 | '''
20 | file("app/src/main/java/org/example/app/ServletImpl.java") << '''
21 | package org.example.app;
22 | public abstract class ServletImpl implements jakarta.servlet.Servlet { }
23 | '''
24 | file("app/src/test/java/org/example/app/test/ServletMock.java") << '''
25 | package org.example.app.test;
26 | public abstract class ServletMock extends org.example.app.ServletImpl { }
27 | '''
28 | appModuleInfoFile << '''
29 | module org.example.app {
30 | requires static jakarta.servlet;
31 | }
32 | '''
33 |
34 | when:
35 | def result = runner('compileTestJava').build()
36 |
37 | then:
38 | result.task(':app:compileTestJava').outcome == TaskOutcome.SUCCESS
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/test/CustomizationTest.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.test
2 |
3 | import org.gradle.testkit.runner.TaskOutcome
4 | import org.gradlex.javamodule.testing.test.fixture.GradleBuild
5 | import spock.lang.Specification
6 |
7 | class CustomizationTest extends Specification {
8 |
9 | @Delegate
10 | GradleBuild build = new GradleBuild()
11 |
12 | def "can customize whitebox test suites in multiple steps"() {
13 | given:
14 | appBuildFile << '''
15 | javaModuleTesting.whitebox(testing.suites["test"]) {
16 | requires.add("org.junit.jupiter.api")
17 | }
18 | javaModuleTesting.whitebox(testing.suites["test"]) {
19 | opensTo.add("org.junit.platform.commons")
20 | }
21 | '''
22 | appModuleInfoFile << '''
23 | module org.example.app {
24 | }
25 | '''
26 |
27 | when:
28 | def result = runTests()
29 |
30 | then:
31 | result.output.contains('Main Module: org.example.app')
32 | result.output.contains('Test Module: org.example.app')
33 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
34 | }
35 |
36 | def "can define whitebox test suite requires in module-info file"() {
37 | given:
38 | appModuleInfoFile << '''
39 | module org.example.app {
40 | }
41 | '''
42 | appWhiteboxTestModuleInfoFile << '''
43 | module org.example.app.test {
44 | requires org.example.app;
45 | requires org.junit.jupiter.api;
46 | }
47 | '''
48 |
49 | when:
50 | def result = runTests()
51 |
52 | then:
53 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
54 | }
55 |
56 | def "can customize whitebox test suites with exportsTo"() {
57 | given:
58 | def mainTest = file("app/src/test/java/org/example/app/test/MainTest.java")
59 | // make test public, so that 'exportsTo org.junit.platform.commons' is sufficient
60 | mainTest.text = mainTest.text.replace('void testApp()' , 'public void testApp()')
61 |
62 | appBuildFile << '''
63 | javaModuleTesting.classpath(testing.suites["test"]) // reset default setup
64 | javaModuleTesting.whitebox(testing.suites["test"]) {
65 | requires.add("org.junit.jupiter.api")
66 | exportsTo.add("org.junit.platform.commons")
67 | }
68 | '''
69 | appModuleInfoFile << '''
70 | module org.example.app {
71 | }
72 | '''
73 |
74 | when:
75 | def result = runTests()
76 |
77 | then:
78 | result.output.contains('Main Module: org.example.app')
79 | result.output.contains('Test Module: org.example.app')
80 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
81 | }
82 |
83 |
84 | def "repetitive blackbox calls on the same test suite have no effect"() {
85 | given:
86 | appBuildFile << '''
87 | javaModuleTesting.blackbox(testing.suites["test"])
88 | javaModuleTesting.blackbox(testing.suites["test"])
89 | dependencies { testImplementation(project(path)) }
90 | '''
91 | appModuleInfoFile << '''
92 | module org.example.app {
93 | exports org.example.app;
94 | }
95 | '''
96 | appTestModuleInfoFile << '''
97 | open module org.example.app.test {
98 | requires org.example.app;
99 | requires org.junit.jupiter.api;
100 | }
101 | '''
102 |
103 | when:
104 | def result = runTests()
105 |
106 | then:
107 | result.output.contains('Main Module: org.example.app')
108 | result.output.contains('Test Module: org.example.app.test')
109 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
110 | }
111 |
112 | def "can use task lock service"() {
113 | given:
114 | appBuildFile.text = 'import org.gradlex.javamodule.testing.TaskLockService\n\n' + appBuildFile.text
115 | appBuildFile << '''
116 | javaModuleTesting.whitebox(testing.suites.getByName("test") {
117 | targets.all {
118 | testTask {
119 | usesService(gradle.sharedServices.registerIfAbsent(TaskLockService.NAME, TaskLockService::class) { maxParallelUsages.set(1) })
120 | }
121 | }
122 | }) {
123 | requires.add("org.junit.jupiter.api")
124 | }
125 | '''
126 | appModuleInfoFile << '''
127 | module org.example.app {
128 | }
129 | '''
130 |
131 | when:
132 | def result = runTests()
133 |
134 | then:
135 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
136 | }
137 |
138 | def "build does not fail when JUnit has no version and the test folder is empty"() {
139 | given:
140 | appTestModuleInfoFile.parentFile.deleteDir()
141 | appBuildFile << '''
142 | testing.suites.withType().all {
143 | useJUnitJupiter("") // <- no version, we want to manage that ourselves
144 | }
145 | '''
146 |
147 | when:
148 | def result = runTests()
149 |
150 | then:
151 | result.task(":app:test").outcome == TaskOutcome.NO_SOURCE
152 | }
153 |
154 | def "build does not fail when JUnit has no version, the test folder is empty and whitebox was manually configured"() {
155 | given:
156 | appTestModuleInfoFile.parentFile.deleteDir()
157 | appBuildFile << '''
158 | testing.suites.withType().all {
159 | useJUnitJupiter("") // <- no version, we want to manage that ourselves
160 | }
161 | javaModuleTesting.whitebox(testing.suites["test"]) {
162 | requires.add("org.junit.jupiter.api")
163 | }
164 | '''
165 |
166 | when:
167 | def result = runTests()
168 |
169 | then:
170 | result.task(":app:test").outcome == TaskOutcome.NO_SOURCE
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/test/JavaModuleDependenciesBridgeTest.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.test
2 |
3 | import org.gradle.testkit.runner.TaskOutcome
4 | import org.gradlex.javamodule.testing.test.fixture.GradleBuild
5 | import spock.lang.Specification
6 |
7 | class JavaModuleDependenciesBridgeTest extends Specification {
8 |
9 | @Delegate
10 | GradleBuild build = new GradleBuild()
11 |
12 | def setup() {
13 | useJavaModuleDependenciesPlugin()
14 | }
15 |
16 | def "respects moduleNameToGA mappings"() {
17 | given:
18 | appBuildFile << '''
19 | javaModuleDependencies {
20 | moduleNameToGA.put("org.example.lib", "org.example:lib")
21 | }
22 | javaModuleTesting.whitebox(testing.suites["test"]) {
23 | requires.add("org.junit.jupiter.api")
24 | requires.add("org.example.lib")
25 | opensTo.add("org.junit.platform.commons")
26 | }
27 | '''
28 | appModuleInfoFile << '''
29 | module org.example.app {
30 | }
31 | '''
32 | libModuleInfoFile << '''
33 | module org.example.lib {
34 | }
35 | '''
36 |
37 | when:
38 | def result = runTests()
39 |
40 | then:
41 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
42 | }
43 |
44 | def "respects moduleNamePrefixToGroup mappings"() {
45 | given:
46 | appBuildFile << '''
47 | javaModuleDependencies {
48 | moduleNamePrefixToGroup.put("org.example.", "org.example")
49 | }
50 | javaModuleTesting.whitebox(testing.suites["test"]) {
51 | requires.add("org.junit.jupiter.api")
52 | requires.add("org.example.lib")
53 | opensTo.add("org.junit.platform.commons")
54 | }
55 | '''
56 | appModuleInfoFile << '''
57 | module org.example.app {
58 | }
59 | '''
60 | libModuleInfoFile << '''
61 | module org.example.lib {
62 | }
63 | '''
64 |
65 | when:
66 | def result = runTests()
67 |
68 | then:
69 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
70 | }
71 |
72 | def "compiles with provides runtime directives"() {
73 | given:
74 | appBuildFile << '''
75 | dependencies.constraints {
76 | javaModuleDependencies {
77 | implementation(gav("org.slf4j", "2.0.3"))
78 | implementation(gav("org.slf4j.simple", "2.0.3"))
79 | }
80 | }
81 | javaModuleDependencies {
82 | moduleNameToGA.put("org.example.lib", "org.example:lib")
83 | }
84 | javaModuleTesting.whitebox(testing.suites["test"]) {
85 | requires.add("org.junit.jupiter.api")
86 | requires.add("org.example.lib")
87 | opensTo.add("org.junit.platform.commons")
88 | }
89 | '''
90 | appModuleInfoFile << '''
91 | module org.example.app {
92 | requires org.slf4j;
93 | requires /*runtime*/ org.slf4j.simple;
94 | }
95 | '''
96 | libModuleInfoFile << '''
97 | module org.example.lib {
98 | }
99 | '''
100 |
101 | when:
102 | def result = runTests()
103 |
104 | then:
105 | result.task(":app:compileTestJava").outcome == TaskOutcome.SUCCESS
106 | }
107 |
108 | def "can be combined with test-fixtures plugins"() {
109 | given:
110 | useTestFixturesPlugin()
111 | appModuleInfoFile << '''
112 | module org.example.app {
113 | }
114 | '''
115 | file("app/src/testFixtures/java/module-info.java") << '''
116 | open module org.example.app.test.fixtures {
117 | requires org.example.app;
118 | }
119 | '''
120 | appBuildFile << '''
121 | javaModuleTesting.whitebox(testing.suites["test"]) {
122 | requires.add("org.junit.jupiter.api")
123 | requires.add("org.example.app.test.fixtures")
124 | }
125 | javaModuleDependencies {
126 | }
127 | '''
128 |
129 | when:
130 | def result = runTests()
131 |
132 | then:
133 | result.task(":app:test").outcome == TaskOutcome.SUCCESS
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/test/groovy/org/gradlex/javamodule/testing/test/fixture/GradleBuild.groovy:
--------------------------------------------------------------------------------
1 | package org.gradlex.javamodule.testing.test.fixture
2 |
3 | import org.gradle.testkit.runner.BuildResult
4 | import org.gradle.testkit.runner.GradleRunner
5 |
6 | import java.lang.management.ManagementFactory
7 | import java.nio.file.Files
8 |
9 | class GradleBuild {
10 |
11 | final File projectDir
12 | final File settingsFile
13 | final File appBuildFile
14 | final File appModuleInfoFile
15 | final File appTestModuleInfoFile
16 | final File appWhiteboxTestModuleInfoFile
17 | final File libBuildFile
18 | final File libModuleInfoFile
19 |
20 | final String gradleVersionUnderTest = System.getProperty("gradleVersionUnderTest")
21 | boolean canUseProjectIsolation = gradleVersionUnderTest == null
22 |
23 | GradleBuild(File projectDir = Files.createTempDirectory("gradle-build").toFile()) {
24 | this.projectDir = projectDir
25 | this.settingsFile = file("settings.gradle.kts")
26 | this.appBuildFile = file("app/build.gradle.kts")
27 | this.appModuleInfoFile = file("app/src/main/java/module-info.java")
28 | this.appTestModuleInfoFile = file("app/src/test/java/module-info.java")
29 | this.appWhiteboxTestModuleInfoFile = file("app/src/test/java9/module-info.java")
30 | this.libBuildFile = file("lib/build.gradle.kts")
31 | this.libModuleInfoFile = file("lib/src/main/java/module-info.java")
32 |
33 | def launcherDependency = gradleVersionUnderTest == '7.4' ?
34 | 'testRuntimeOnly("org.junit.platform:junit-platform-launcher")' : ''
35 |
36 | settingsFile << '''
37 | pluginManagement {
38 | plugins { id("org.gradlex.java-module-dependencies") version "1.8" }
39 | }
40 | dependencyResolutionManagement { repositories.mavenCentral() }
41 | includeBuild(".")
42 | rootProject.name = "test-project"
43 | include("app", "lib")
44 | '''
45 | appBuildFile << """
46 | plugins {
47 | id("org.gradlex.java-module-testing")
48 | id("application")
49 | }
50 | group = "org.example"
51 | dependencies {
52 | testImplementation(platform("org.junit:junit-bom:5.9.0"))
53 | $launcherDependency
54 | }
55 | application {
56 | mainModule.set("org.example.app")
57 | mainClass.set("org.example.app.Main")
58 | }
59 | tasks.test {
60 | testLogging.showStandardStreams = true
61 | }
62 | """
63 | file("app/src/main/java/org/example/app/Main.java") << '''
64 | package org.example.app;
65 |
66 | public class Main {
67 | public void main(String... args) {
68 | }
69 | }
70 | '''
71 | file("app/src/test/java/org/example/app/test/MainTest.java") << '''
72 | package org.example.app.test;
73 |
74 | import org.junit.jupiter.api.Test;
75 | import org.example.app.Main;
76 |
77 | public class MainTest {
78 |
79 | @Test
80 | void testApp() {
81 | new Main();
82 | System.out.println("Main Module: " + Main.class.getModule().getName());
83 | System.out.println("Test Module: " + MainTest.class.getModule().getName());
84 | }
85 | }
86 | '''
87 |
88 | libBuildFile << '''
89 | plugins {
90 | id("org.gradlex.java-module-testing")
91 | id("java-library")
92 | }
93 | group = "org.example"
94 | '''
95 | }
96 |
97 | void useJavaModuleDependenciesPlugin() {
98 | canUseProjectIsolation = false // 'java-module-dependencies' not yet fully compatible
99 | appBuildFile.text = appBuildFile.text.replace('plugins {', 'plugins { id("org.gradlex.java-module-dependencies")')
100 | libBuildFile.text = libBuildFile.text.replace('plugins {', 'plugins { id("org.gradlex.java-module-dependencies")')
101 | }
102 |
103 | def useTestFixturesPlugin() {
104 | appBuildFile.text = appBuildFile.text.replace('plugins {', 'plugins { id("java-test-fixtures");')
105 | libBuildFile.text = libBuildFile.text.replace('plugins {', 'plugins { id("java-test-fixtures");')
106 | }
107 |
108 | File file(String path) {
109 | new File(projectDir, path).tap {
110 | it.getParentFile().mkdirs()
111 | }
112 | }
113 |
114 | BuildResult build() {
115 | runner('build').build()
116 | }
117 |
118 | BuildResult run() {
119 | runner('run').build()
120 | }
121 |
122 | BuildResult runTests() {
123 | runner(':app:test').build()
124 | }
125 |
126 | BuildResult fail() {
127 | runner('build').buildAndFail()
128 | }
129 |
130 | GradleRunner runner(String... args) {
131 | List latestFeaturesArgs = canUseProjectIsolation ? [
132 | '-Dorg.gradle.unsafe.isolated-projects=true'
133 | ] : []
134 | GradleRunner.create()
135 | .forwardOutput()
136 | .withPluginClasspath()
137 | .withProjectDir(projectDir)
138 | .withArguments(Arrays.asList(args) + latestFeaturesArgs + '-s' + '--configuration-cache')
139 | .withDebug(ManagementFactory.getRuntimeMXBean().getInputArguments().toString().contains("-agentlib:jdwp")).with {
140 | gradleVersionUnderTest ? it.withGradleVersion(gradleVersionUnderTest) : it
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/test/java/org/gradlex/javamodule/testing/test/samples/PluginBuildLocationSampleModifier.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.test.samples;
18 |
19 | import org.gradle.exemplar.model.Command;
20 | import org.gradle.exemplar.model.Sample;
21 | import org.gradle.exemplar.test.runner.SampleModifier;
22 |
23 | import java.io.File;
24 | import java.util.stream.Collectors;
25 | import java.util.stream.Stream;
26 |
27 | public class PluginBuildLocationSampleModifier implements SampleModifier {
28 | @Override
29 | public Sample modify(Sample sampleIn) {
30 | Command cmd = sampleIn.getCommands().remove(0);
31 | File pluginProjectDir = new File(".");
32 | sampleIn.getCommands().add(
33 | new Command(new File(pluginProjectDir, "gradlew").getAbsolutePath(),
34 | cmd.getExecutionSubdirectory(),
35 | Stream.concat(cmd.getArgs().stream(), Stream.of("build", "--warning-mode=all","-PpluginLocation=" + pluginProjectDir.getAbsolutePath())).collect(Collectors.toList()),
36 | cmd.getFlags(),
37 | cmd.getExpectedOutput(),
38 | cmd.isExpectFailure(),
39 | true,
40 | cmd.isAllowDisorderedOutput(),
41 | cmd.getUserInputs()));
42 | return sampleIn;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/java/org/gradlex/javamodule/testing/test/samples/SamplesTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright the GradleX team.
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 | * http://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.gradlex.javamodule.testing.test.samples;
18 |
19 | import org.gradle.exemplar.test.runner.SampleModifiers;
20 | import org.gradle.exemplar.test.runner.SamplesRoot;
21 | import org.gradle.exemplar.test.runner.SamplesRunner;
22 | import org.junit.runner.RunWith;
23 |
24 | @RunWith(SamplesRunner.class)
25 | @SamplesRoot("samples")
26 | @SampleModifiers(PluginBuildLocationSampleModifier.class)
27 | public class SamplesTest {
28 |
29 | }
30 |
--------------------------------------------------------------------------------