Skip to content

Commit

Permalink
GEODE-10321: Acceptance test for Geode access to JDK internals (apach…
Browse files Browse the repository at this point in the history
…e#7772)

* GEODE-10321: Acceptance test for Geode access to JDK internals

Add acceptance tests to demonstrate two ways to give Geode access to
encapsulated fields of a JDK 17 class:

- Tell Gfsh to open the class's package when starting a Geode member.
- Tell Gfsh to use `open-all-jdk-packages-linux-openjdk-17` as an
  argument file when starting a Geode member.

Add another test to demonstrate the kind of error that happens when
Geode cannot access an encapsulated field of a JDK 17 class.

* Clean up

Remove unnecessary dependency on JUnit 5 rule migration support.

Start server without default server.

Other cleanup.
  • Loading branch information
demery-pivotal authored Jun 8, 2022
1 parent 425bdb8 commit b20a6b0
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/

package org.apache.geode.jdk;

import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.apache.geode.test.util.JarUtils.createJarWithClasses;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;

import org.apache.geode.test.junit.rules.FolderRule;
import org.apache.geode.test.junit.rules.gfsh.GfshRule;
import org.apache.geode.test.junit.rules.gfsh.GfshScript;
import org.apache.geode.test.version.JavaVersions;

/**
* Test several ways to make normally inaccessible JDK packages accessible on JDK 17.
*/
public class JdkEncapsulationTest {
@Rule(order = 0)
public final FolderRule folderRule = new FolderRule();

@Rule(order = 1)
public final GfshRule gfshRule = new GfshRule(folderRule::getFolder);

private String startServer;
private GfshScript traverseEncapsulatedJdkObject;

@BeforeClass
public static void validOnlyOnJdk17AndLater() {
assumeThat(JavaVersions.current().specificationVersion())
.isGreaterThanOrEqualTo(17);
}

@Before
public void startLocatorWithObjectTraverserFunction() throws IOException {
Path jarPath = folderRule.getFolder().toPath().resolve("traverse-encapsulated-jdk-object.jar");
createJarWithClasses(jarPath, TraverseEncapsulatedJdkObject.class);

int locatorPort = getRandomAvailableTCPPort();
String locators = "localhost[" + locatorPort + "]";

startServer = "start server --name=server --disable-default-server --locators=" + locators;
traverseEncapsulatedJdkObject = GfshScript
.of("connect --locator=" + locators)
.and("execute function --id=" + TraverseEncapsulatedJdkObject.ID);

GfshScript
.of("start locator --port=" + locatorPort)
.and("deploy --jar=" + jarPath)
.execute(gfshRule);
}

// If this test fails, it means the object we're trying to traverse has no inaccessible fields,
// and so is not useful for the other tests. If it fails, update TraverseInaccessibleJdkObject
// to use a type that actually has inaccessible fields.
@Test
public void cannotMakeEncapsulatedFieldsAccessibleByDefault() {
gfshRule.execute(startServer); // No JDK options

String traversalResult = traverseEncapsulatedJdkObject
.expectExitCode(1) // Because we did not open any JDK packages.
.execute(gfshRule)
.getOutputText();

assertThat(traversalResult)
.as("result of traversing %s", TraverseEncapsulatedJdkObject.OBJECT.getClass())
.contains("Exception: java.lang.reflect.InaccessibleObjectException");
}

@Test
public void canMakeEncapsulatedFieldsAccessibleInExplicitlyOpenedPackages() {
String objectPackage = TraverseEncapsulatedJdkObject.OBJECT.getClass().getPackage().getName();
String objectModule = TraverseEncapsulatedJdkObject.MODULE;

String openThePackageOfTheEncapsulatedJdkObject =
String.format(" --J=--add-opens=%s/%s=ALL-UNNAMED", objectModule, objectPackage);

gfshRule.execute(startServer + openThePackageOfTheEncapsulatedJdkObject);

traverseEncapsulatedJdkObject
.expectExitCode(0) // Because we opened the encapsulated object's package.
.execute(gfshRule);
}

@Test
public void canMakeEncapsulatedFieldsAccessibleInPackagesOpenedByArgumentFile() {
// A few of the packages opened by this argument file are specific to Linux JDKs. Running this
// test on other operating systems might emit some warnings, but they are harmless.
String argumentFileName = "open-all-jdk-packages-linux-openjdk-17";
Path argumentFilePath = Paths.get(System.getenv("GEODE_HOME"), "config", argumentFileName)
.toAbsolutePath().normalize();

String useArgumentFile = " --J=@" + argumentFilePath;

gfshRule.execute(startServer + useArgumentFile);

traverseEncapsulatedJdkObject
.expectExitCode(0) // Because the argument file opens all JDK packages.
.execute(gfshRule);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/

package org.apache.geode.jdk;

import java.math.BigDecimal;

import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.internal.size.ObjectTraverser;
import org.apache.geode.internal.size.ObjectTraverser.Visitor;

public class TraverseEncapsulatedJdkObject implements Function<Void> {
private static final Visitor TRAVERSE_ENTIRE_OBJECT_GRAPH = (parent, object) -> true;
private final ObjectTraverser traverser = new ObjectTraverser();

// OBJECT must have a JDK type with inaccessible fields, defined in a package that Gfsh does
// not open by default.
static final BigDecimal OBJECT = BigDecimal.ONE;
// MODULE must be the module that defines OBJECT's type.
static final String MODULE = "java.base";
static final String ID = "traverse-big-decimal";

@Override
public String getId() {
return ID;
}

@Override
public void execute(FunctionContext<Void> context) {
try {
traverser.breadthFirstSearch(OBJECT, TRAVERSE_ENTIRE_OBJECT_GRAPH, false);
} catch (IllegalAccessException e) {
context.getResultSender().sendException(e);
return;
}
context.getResultSender().lastResult("OK");
}
}
59 changes: 59 additions & 0 deletions geode-junit/src/main/java/org/apache/geode/test/util/JarUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/

package org.apache.geode.test.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import org.apache.commons.io.IOUtils;

public class JarUtils {
public static Path createJarWithClasses(Path jarPath, Class<?>... classes) throws IOException {
Path jarFile = Files.createFile(jarPath);
try (OutputStream outputStream = Files.newOutputStream(jarFile);
JarOutputStream jarOutputStream = new JarOutputStream(outputStream)) {
Arrays.stream(classes)
.forEach(clazz -> writeClassTo(clazz, jarOutputStream));
}
return jarFile;
}

private static void writeClassTo(Class<?> clazz, JarOutputStream jarOutputStream) {
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
try (InputStream classInputStream = clazz.getClassLoader().getResourceAsStream(classAsPath)) {
if (classInputStream == null) {
throw new IllegalArgumentException("Cannot read class definition for " + className);
}
JarEntry classEntry = new JarEntry(classAsPath);
classEntry.setTime(System.currentTimeMillis());
byte[] classBytes = IOUtils.toByteArray(classInputStream);
jarOutputStream.putNextEntry(classEntry);
jarOutputStream.write(classBytes);
jarOutputStream.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

0 comments on commit b20a6b0

Please sign in to comment.