Skip to content

Commit 6252826

Browse files
authored
Add arch test to detect usage of shared internal code (#6978)
1 parent 8a8cd79 commit 6252826

File tree

2 files changed

+174
-18
lines changed

2 files changed

+174
-18
lines changed

all/build.gradle.kts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,13 @@ plugins {
55
description = "OpenTelemetry All"
66
otelJava.moduleName.set("io.opentelemetry.all")
77

8-
tasks {
9-
// We don't compile much here, just some API boundary tests. This project is mostly for
10-
// aggregating jacoco reports and it doesn't work if this isn't at least as high as the
11-
// highest supported Java version in any of our projects. All of our
12-
// projects target Java 8 except :exporters:http-sender:jdk, which targets
13-
// Java 11
14-
withType(JavaCompile::class) {
15-
options.release.set(11)
16-
}
17-
18-
val testJavaVersion: String? by project
19-
if (testJavaVersion == "8") {
20-
test {
21-
enabled = false
22-
}
23-
}
24-
}
25-
268
// Skip OWASP dependencyCheck task on test module
279
dependencyCheck {
2810
skip = true
2911
}
3012

3113
val testTasks = mutableListOf<Task>()
14+
val jarTasks = mutableListOf<Jar>()
3215

3316
dependencies {
3417
rootProject.subprojects.forEach { subproject ->
@@ -41,13 +24,68 @@ dependencies {
4124
subproject.tasks.withType<Test>().configureEach {
4225
testTasks.add(this)
4326
}
27+
subproject.tasks.withType<Jar>().forEach {
28+
if (it.archiveClassifier.get().isEmpty() && !it.name.contains("jmh")) {
29+
jarTasks.add(it)
30+
}
31+
}
4432
}
4533
}
4634
}
4735

4836
testImplementation("com.tngtech.archunit:archunit-junit5")
4937
}
5038

39+
// Custom task type for writing artifacts and jars - configuration cache compatible
40+
abstract class WriteArtifactsAndJars : DefaultTask() {
41+
@get:Input
42+
abstract val artifactData: MapProperty<String, String>
43+
44+
@get:OutputFile
45+
abstract val outputFile: RegularFileProperty
46+
47+
@TaskAction
48+
fun writeFile() {
49+
val file = outputFile.get().asFile
50+
file.parentFile.mkdirs()
51+
val content = artifactData.get().entries.joinToString("\n") { (baseName, filePath) ->
52+
"$baseName:$filePath"
53+
}
54+
file.writeText(content)
55+
}
56+
}
57+
58+
val artifactsAndJarsFile = layout.buildDirectory.file("artifacts_and_jars.txt")
59+
60+
val writeArtifactsAndJars = tasks.register<WriteArtifactsAndJars>("writeArtifactsAndJars") {
61+
// Set up task dependencies
62+
dependsOn(jarTasks)
63+
64+
// Configure the task inputs and outputs using providers
65+
artifactData.set(provider {
66+
jarTasks.associate { jar ->
67+
jar.archiveBaseName.get() to jar.archiveFile.get().asFile.absolutePath
68+
}
69+
})
70+
outputFile.set(artifactsAndJarsFile)
71+
}
72+
73+
tasks {
74+
// This module depends on all published subprojects (for JaCoCo coverage aggregation and
75+
// arch tests). Since :exporters:http-sender:jdk targets Java 11, the compiler needs
76+
// release 11 to resolve its classes on the classpath.
77+
withType(JavaCompile::class) {
78+
options.release.set(11)
79+
}
80+
81+
val testJavaVersion: String? by project
82+
test {
83+
enabled = testJavaVersion != "8"
84+
dependsOn(writeArtifactsAndJars)
85+
environment("ARTIFACTS_AND_JARS", artifactsAndJarsFile.get().asFile.absolutePath)
86+
}
87+
}
88+
5189
// https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage.html
5290

5391
val sourcesPath by configurations.creating {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.all;
7+
8+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
9+
10+
import com.tngtech.archunit.base.DescribedPredicate;
11+
import com.tngtech.archunit.core.domain.JavaClass;
12+
import com.tngtech.archunit.core.domain.JavaClasses;
13+
import com.tngtech.archunit.core.importer.ClassFileImporter;
14+
import com.tngtech.archunit.lang.syntax.elements.ClassesShouldConjunction;
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.util.List;
20+
import java.util.Set;
21+
import java.util.jar.JarFile;
22+
import java.util.logging.Level;
23+
import java.util.logging.Logger;
24+
import java.util.stream.Collectors;
25+
import java.util.stream.Stream;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.Arguments;
28+
import org.junit.jupiter.params.provider.MethodSource;
29+
30+
class NoSharedInternalCodeTest {
31+
32+
private static final Set<String> exemptions =
33+
Set.of(
34+
"opentelemetry-api-incubator",
35+
"opentelemetry-exporter-common",
36+
"opentelemetry-exporter-logging",
37+
"opentelemetry-exporter-logging-otlp",
38+
"opentelemetry-exporter-prometheus",
39+
"opentelemetry-exporter-zipkin",
40+
"opentelemetry-extension-trace-propagators",
41+
"opentelemetry-opencensus-shim",
42+
"opentelemetry-sdk-common",
43+
"opentelemetry-sdk-logs",
44+
"opentelemetry-sdk-metrics",
45+
"opentelemetry-sdk-testing",
46+
"opentelemetry-sdk-trace",
47+
"opentelemetry-sdk-extension-autoconfigure",
48+
"opentelemetry-sdk-extension-autoconfigure-spi",
49+
"opentelemetry-sdk-extension-incubator",
50+
"opentelemetry-sdk-extension-jaeger-remote-sampler",
51+
"opentelemetry-exporter-otlp",
52+
"opentelemetry-exporter-otlp-common",
53+
"opentelemetry-exporter-sender-grpc-managed-channel",
54+
"opentelemetry-exporter-sender-jdk",
55+
"opentelemetry-exporter-sender-okhttp");
56+
57+
private static final String OTEL_BASE_PACKAGE = "io.opentelemetry";
58+
private static final Logger logger = Logger.getLogger(NoSharedInternalCodeTest.class.getName());
59+
60+
@ParameterizedTest
61+
@MethodSource("artifactsAndJars")
62+
void noSharedInternalCode(String artifactId, String absolutePath) throws IOException {
63+
try (JarFile jarFile = new JarFile(new File(absolutePath))) {
64+
JavaClasses artifactClasses = new ClassFileImporter().importJar(jarFile);
65+
66+
Set<String> artifactOtelPackages =
67+
artifactClasses.stream()
68+
.map(JavaClass::getPackageName)
69+
.filter(packageName -> packageName.startsWith(OTEL_BASE_PACKAGE))
70+
.collect(Collectors.toSet());
71+
72+
ClassesShouldConjunction noSharedInternalCodeRule =
73+
noClasses()
74+
.that()
75+
.resideInAnyPackage(artifactOtelPackages.toArray(new String[0]))
76+
.should()
77+
.dependOnClassesThat(
78+
new DescribedPredicate<>(
79+
"are in internal modules of other opentelemetry artifacts") {
80+
@Override
81+
public boolean test(JavaClass javaClass) {
82+
String packageName = javaClass.getPackageName();
83+
return packageName.startsWith(OTEL_BASE_PACKAGE)
84+
&& packageName.contains(".internal")
85+
&& !artifactOtelPackages.contains(packageName);
86+
}
87+
});
88+
89+
try {
90+
noSharedInternalCodeRule
91+
.as(artifactId + " should not use internal code from other artifacts")
92+
.check(artifactClasses);
93+
// To view artifacts which do not contain shared internal code, change test log level or
94+
// increase log level of this statement to WARNING
95+
logger.log(Level.INFO, artifactId + " does not contain shared internal code");
96+
} catch (AssertionError e) {
97+
if (exemptions.contains(artifactId)) {
98+
// To view details, remove from exemptions list
99+
logger.log(
100+
Level.WARNING,
101+
artifactId + " contains shared internal code but is temporarily exempt");
102+
} else {
103+
throw e;
104+
}
105+
}
106+
}
107+
}
108+
109+
private static Stream<Arguments> artifactsAndJars() throws IOException {
110+
List<String> lines = Files.readAllLines(Path.of(System.getenv("ARTIFACTS_AND_JARS")));
111+
return lines.stream()
112+
.map(
113+
line -> {
114+
String[] parts = line.split(":", 2);
115+
return Arguments.of(parts[0], parts[1]);
116+
});
117+
}
118+
}

0 commit comments

Comments
 (0)