Skip to content

Commit

Permalink
feat(compliance): produce compliance test reports (#1368)
Browse files Browse the repository at this point in the history
Generates reports (xUnit or JSON) out of compliance test executions
so that we can assert conformance based on the spec.
  • Loading branch information
RomainMuller committed Apr 14, 2020
1 parent 10ddd10 commit 11ef55d
Show file tree
Hide file tree
Showing 21 changed files with 419 additions and 131 deletions.
3 changes: 3 additions & 0 deletions packages/@jsii/dotnet-runtime-test/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ test/Directory.Build.props
# consumed by generate.sh
!*.t.js

# Result of tests (xUnit format)
TestResults.xml

*.nupkg
bin/
cli/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

<VSTestLogger>xunit</VSTestLogger>
</PropertyGroup>

<ItemGroup>
Expand All @@ -19,6 +21,7 @@
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="XunitXml.TestLogger" Version="2.1.26" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Amazon.JSII.Runtime.IntegrationTests
/// <summary>
/// Ported from packages/jsii-java-runtime/src/test/java/org/jsii/testing/ComplianceTest.java.
/// </summary>
public sealed class ComplianceTests : IClassFixture<ServiceContainerFixture>
public sealed class ComplianceTests : IClassFixture<ServiceContainerFixture>, IDisposable
{
class RuntimeException : Exception
{
Expand All @@ -33,9 +33,17 @@ public RuntimeException(string message)

const string Prefix = nameof(IntegrationTests) + ".Compliance.";

private readonly IDisposable _serviceContainerFixture;

public ComplianceTests(ITestOutputHelper outputHelper, ServiceContainerFixture serviceContainerFixture)
{
serviceContainerFixture.SetOverride(outputHelper);
_serviceContainerFixture = serviceContainerFixture;
}

void IDisposable.Dispose()
{
_serviceContainerFixture.Dispose();
}

[Fact(DisplayName = Prefix + nameof(PrimitiveTypes))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Amazon.JSII.Runtime.Deputy;
using Amazon.JSII.Runtime.Services;
using Xunit.Abstractions;

Expand All @@ -21,9 +22,10 @@ public void SetOverride(ITestOutputHelper outputHelper)
}
}

public void Dispose()
void IDisposable.Dispose()
{
ServiceContainer.ServiceProviderOverride = null;
JsiiTypeAttributeBase.Reset();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public XUnitLogger(ITestOutputHelper output, string categoryName)

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_output.WriteLine($"[{_categoryName}] {state?.ToString()}");
var str = state?.ToString() ?? "";
// Only log lines starting with > or < (kernel traces)
if (str.StartsWith(">") || str.StartsWith("<"))
{
_output.WriteLine(str);
}
}

public IDisposable BeginScope<TState>(TState state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public DeputyProps(object[]? arguments = null)
protected DeputyBase(DeputyProps? props = null)
{
var type = GetType();
JsiiTypeAttributeBase.Load(type.Assembly);

// If this is a native object, it won't have any jsii metadata.
var attribute = ReflectionUtils.GetClassAttribute(type);
Expand Down Expand Up @@ -125,6 +126,8 @@ protected static T GetStaticProperty<T>(System.Type type, [CallerMemberName] str
type = type ?? throw new ArgumentNullException(nameof(type));
propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));

JsiiTypeAttributeBase.Load(type.Assembly);

var classAttribute = ReflectionUtils.GetClassAttribute(type)!;
var propertyAttribute = GetStaticPropertyAttribute(type, propertyName);

Expand Down Expand Up @@ -174,6 +177,8 @@ protected static void SetStaticProperty<T>(System.Type type, T value, [CallerMem
type = type ?? throw new ArgumentNullException(nameof(type));
propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));

JsiiTypeAttributeBase.Load(type.Assembly);

var classAttribute = ReflectionUtils.GetClassAttribute(type)!;
var propertyAttribute = GetStaticPropertyAttribute(type, propertyName);

Expand Down Expand Up @@ -228,6 +233,8 @@ protected void InvokeInstanceVoidMethod(System.Type[] parameterTypes, object[] a
[return: MaybeNull]
protected static T InvokeStaticMethod<T>(System.Type type, System.Type[] parameterTypes, object[] arguments, [CallerMemberName] string methodName = "")
{
JsiiTypeAttributeBase.Load(type.Assembly);

var methodAttribute = GetStaticMethodAttribute(type, methodName, parameterTypes);
var classAttribute = ReflectionUtils.GetClassAttribute(type)!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected JsiiTypeAttributeBase(Type nativeType, string fullyQualifiedName)
Load(nativeType.Assembly);
}

private static void Load(Assembly assembly)
internal static void Load(Assembly assembly)
{
if (ProcessedAssemblies.Contains(GetAssemblyKey(assembly)))
{
Expand Down Expand Up @@ -62,6 +62,15 @@ private static void Load(Assembly assembly)
string GetAssemblyKey(Assembly assemblyReference) => assemblyReference.GetName().FullName;
}

/// <summary>
/// This method is here for the test harness to be able to fully reset the execution engine, and trigger a
/// reload of all assemblies. This should not be called by anything other than compliance tests.
/// </summary>
internal static void Reset()
{
ProcessedAssemblies.Clear();
}

public string FullyQualifiedName { get; }
}
}
8 changes: 4 additions & 4 deletions packages/@jsii/java-runtime-test/pom.xml.t.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ process.stdout.write(`<?xml version="1.0" encoding="UTF-8"?>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.1</version>
<scope>test</scope>
</dependency>
Expand Down
3 changes: 3 additions & 0 deletions packages/@jsii/java-runtime-test/project/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties

# Ignore our test files
compliance-report.json

# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package software.amazon.jsii;

import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class ComplianceSuiteHarness implements BeforeEachCallback, AfterEachCallback, AfterAllCallback {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectNode result = objectMapper.createObjectNode();
private final Map<String, List<String>> kernelTraces = new HashMap<>();

@Override
public void beforeEach(final ExtensionContext extensionContext) throws Exception {
final List<String> trace = new ArrayList<>();
kernelTraces.put(extensionContext.getUniqueId(), trace);
JsiiRuntime.messageInspector.set((message, type) -> {
final String prefix = type == MessageInspector.MessageType.Request ? "<" : ">";
try {
trace.add(String.format("%s %s%n", prefix, objectMapper.writeValueAsString(message)));
System.err.printf("%s %s%n", prefix, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(message));
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
});

JsiiEngine.reset();
}

@Override
public void afterEach(final ExtensionContext extensionContext) {
JsiiRuntime.messageInspector.remove();
final ObjectNode entry = result.putObject(String.format("%s.%s",
extensionContext.getRequiredTestClass().getSimpleName(), extensionContext.getRequiredTestMethod().getName()));
entry.put("status", extensionContext.getExecutionException().isPresent() ? "failure" : "success");
entry.putPOJO("kernelTrace", kernelTraces.remove(extensionContext.getUniqueId()));
}

@Override
public void afterAll(final ExtensionContext extensionContext) throws IOException {
final File file = new File("./compliance-report.json");
try (final OutputStream os = new FileOutputStream(file)) {
this.objectMapper.writer(new DefaultPrettyPrinter().withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE)).writeValue(os, this.result);
} catch (IOException ioe) {
System.err.println("Failed writing test report: " + ioe.getMessage());
throw ioe;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package software.amazon.jsii;

import org.junit.Test;
import org.junit.jupiter.api.Test;

import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.*;
import static software.amazon.jsii.JsiiVersion.JSII_RUNTIME_VERSION;

public final class JsiiVersionTest {
Expand All @@ -15,19 +14,22 @@ public void compatibleVersions() {
JsiiRuntime.assertVersionCompatible("0.7.0+cdfe0", "0.7.0+abcd111");
}

@Test(expected = JsiiException.class)
@Test
public void incompatibleVersions_1() {
JsiiRuntime.assertVersionCompatible("0.7.0", "0.7.1");
assertThrows(JsiiException.class,
() -> JsiiRuntime.assertVersionCompatible("0.7.0", "0.7.1"));
}

@Test(expected = JsiiException.class)
@Test
public void incompatibleVersions_2() {
JsiiRuntime.assertVersionCompatible("0.7.0", "0.7");
assertThrows(JsiiException.class,
() -> JsiiRuntime.assertVersionCompatible("0.7.0", "0.7"));
}

@Test(expected = JsiiException.class)
@Test
public void incompatibleVersions_3() {
JsiiRuntime.assertVersionCompatible("0.7.0+abcd", "1.2.0+abcd");
assertThrows(JsiiException.class,
() -> JsiiRuntime.assertVersionCompatible("0.7.0+abcd", "1.2.0+abcd"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package software.amazon.jsii;

import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.util.stream.Stream;

/**
* This wonderful utility can be used to reload classes regardless of whether it was already loaded by the current
* ClassLoader or not. This is particularly useful when a test needs to go through the burden of checking static
* initialization is happening as designed.
*
* It leverages black magic in the form of shameless down-casting of classloaders to URLClassLoader and may or may not
* spectacularly blow up on new JVM major versions.
*
* THIS IS A DRAGONS LAIR AND YOU SHOULD TREAD CAREFULLY, SO AS NOT TO STEP ON A DRAGON'S TAIL.
*/
public final class ReloadingClassLoader extends URLClassLoader {
/**
* Reloads one or more classes, returning the newly loaded version of the first one.
*
* @param parent is the parent ClassLoader to use for classes that do not need to be re-loaded.
* @param clazz is the first class that needs to be reloaded.
* @param others a list of any other class that also needs to be reloaded.
* @param <T> the static type of the reloaded {@code class}.
*
* @return the reloaded version of {@code class}.
*/
@SuppressWarnings("unchecked")
public static <T> Class<T> reload(final ClassLoader parent, final Class<T> clazz, final Class<?> ...others) {
final ClassLoader cl = new ReloadingClassLoader(parent, Stream.concat(Stream.of(clazz), Stream.of(others)).toArray(Class[]::new));
try {
return (Class<T>)cl.loadClass(binaryNameOf(clazz));
} catch (final ClassNotFoundException cnfe) {
// This is theoretically impossible!
throw new RuntimeException(cnfe);
}
}

/**
* Obtains the "binary name" of a class. This is needed because ClassLoaders expect a binary name, and not a
* canonical class name. Binary names have the pesky "$" separator instead of a "." for member classes.
*
* @param clazz the class which binary name is needed.
*
* @return {@code clazz}' binary name.
*/
private static String binaryNameOf(final Class<?> clazz) {
if (!clazz.isMemberClass()) {
return clazz.getCanonicalName();
}
final Class<?> declaringClass = clazz.getDeclaringClass();
return String.format("%s$%s", binaryNameOf(declaringClass), clazz.getSimpleName());
}

private final Class<?>[] toReload;

private ReloadingClassLoader(final ClassLoader parent, final Class<?> ...toReload) {
super(
Stream.of(toReload)
.flatMap(clazz -> Stream.of(((URLClassLoader) clazz.getClassLoader()).getURLs()))
.toArray(URL[]::new),
parent
);
this.toReload = toReload;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (Stream.of(this.toReload).map(ReloadingClassLoader::binaryNameOf).noneMatch(clazz -> clazz.equals(name))) {
// Not to be reloaded - delegate to the standard flow.
return super.loadClass(name, resolve);
}
// Class is to be reloaded. Conveniently, "findClass" does just that!
final Class<?> result = this.findClass(name);
if (resolve) {
this.resolveClass(result);
}
return result;
}
}
Loading

0 comments on commit 11ef55d

Please sign in to comment.