Skip to content

Commit 41f4b0e

Browse files
committed
[Java] Extend property-based tests to exercise Java DTOs.
The added test (non-exhaustively) checks the property: ``` ∀ msg ∈ MessageSchemas, ∀ encoding ∈ EncodingsOf(msg), encoding = dtoEncode(dtoDecode(encoding)) ``` The added test shows some compilation failures, which need to be resolved before enabling it.
1 parent 328cc42 commit 41f4b0e

File tree

3 files changed

+319
-1
lines changed

3 files changed

+319
-1
lines changed

sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/java/JavaGenerator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public JavaGenerator(
160160
{
161161
Verify.notNull(ir, "ir");
162162
Verify.notNull(outputManager, "outputManager");
163+
Verify.notNull(precedenceChecks, "precedenceChecks");
163164

164165
this.ir = ir;
165166
this.shouldSupportTypesPackageNames = shouldSupportTypesPackageNames;

sbe-tool/src/propertyTest/java/uk/co/real_logic/sbe/properties/DtosPropertyTest.java

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,42 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package uk.co.real_logic.sbe.properties;
1817

1918
import net.jqwik.api.*;
19+
import uk.co.real_logic.sbe.generation.common.PrecedenceChecks;
2020
import uk.co.real_logic.sbe.generation.cpp.CppDtoGenerator;
2121
import uk.co.real_logic.sbe.generation.cpp.CppGenerator;
2222
import uk.co.real_logic.sbe.generation.cpp.NamespaceOutputManager;
2323
import uk.co.real_logic.sbe.generation.csharp.CSharpDtoGenerator;
2424
import uk.co.real_logic.sbe.generation.csharp.CSharpGenerator;
2525
import uk.co.real_logic.sbe.generation.csharp.CSharpNamespaceOutputManager;
26+
import uk.co.real_logic.sbe.generation.java.JavaDtoGenerator;
27+
import uk.co.real_logic.sbe.generation.java.JavaGenerator;
28+
import uk.co.real_logic.sbe.ir.generated.MessageHeaderDecoder;
2629
import uk.co.real_logic.sbe.properties.arbitraries.SbeArbitraries;
30+
import uk.co.real_logic.sbe.properties.utils.InMemoryOutputManager;
31+
import org.agrona.DirectBuffer;
32+
import org.agrona.ExpandableArrayBuffer;
2733
import org.agrona.IoUtil;
34+
import org.agrona.MutableDirectBuffer;
35+
import org.agrona.concurrent.UnsafeBuffer;
2836
import org.agrona.io.DirectBufferInputStream;
2937

3038
import java.io.IOException;
3139
import java.io.InputStream;
3240
import java.io.OutputStream;
41+
import java.lang.reflect.InvocationTargetException;
42+
import java.lang.reflect.Method;
43+
import java.net.URLClassLoader;
3344
import java.nio.charset.StandardCharsets;
3445
import java.nio.file.Files;
3546
import java.nio.file.Path;
3647
import java.util.Arrays;
3748

49+
import static uk.co.real_logic.sbe.SbeTool.JAVA_DEFAULT_DECODING_BUFFER_TYPE;
50+
import static uk.co.real_logic.sbe.SbeTool.JAVA_DEFAULT_ENCODING_BUFFER_TYPE;
51+
3852
@SuppressWarnings("ReadWriteStringCanBeUsed")
3953
public class DtosPropertyTest
4054
{
@@ -44,6 +58,101 @@ public class DtosPropertyTest
4458
private static final String CPP_EXECUTABLE = System.getProperty("sbe.tests.cpp.executable", "g++");
4559
private static final boolean KEEP_DIR_ON_FAILURE = Boolean.parseBoolean(
4660
System.getProperty("sbe.tests.keep.dir.on.failure", "true"));
61+
private final ExpandableArrayBuffer outputBuffer = new ExpandableArrayBuffer();
62+
63+
// @Test
64+
// void oneRun() throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException,
65+
// IllegalAccessException
66+
// {
67+
// javaDtoEncodeShouldBeTheInverseOfDtoDecode(encodedMessage().sample());
68+
// }
69+
70+
@Property
71+
@Disabled
72+
void javaDtoEncodeShouldBeTheInverseOfDtoDecode(
73+
@ForAll("encodedMessage") final SbeArbitraries.EncodedMessage encodedMessage
74+
) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
75+
IllegalAccessException
76+
{
77+
final String packageName = encodedMessage.ir().applicableNamespace();
78+
final InMemoryOutputManager outputManager = new InMemoryOutputManager(packageName);
79+
80+
boolean success = false;
81+
82+
try
83+
{
84+
try
85+
{
86+
new JavaGenerator(
87+
encodedMessage.ir(),
88+
JAVA_DEFAULT_ENCODING_BUFFER_TYPE,
89+
JAVA_DEFAULT_DECODING_BUFFER_TYPE,
90+
false,
91+
false,
92+
false,
93+
false,
94+
PrecedenceChecks.newInstance(new PrecedenceChecks.Context()),
95+
outputManager)
96+
.generate();
97+
98+
new JavaDtoGenerator(encodedMessage.ir(), outputManager)
99+
.generate();
100+
}
101+
catch (final Exception generationException)
102+
{
103+
throw new AssertionError(
104+
"Code generation failed.\n\n" +
105+
"SCHEMA:\n" + encodedMessage.schema(),
106+
generationException);
107+
}
108+
109+
try (URLClassLoader generatedClassLoader = outputManager.compileGeneratedSources())
110+
{
111+
final Class<?> dtoClass =
112+
generatedClassLoader.loadClass(packageName + ".TestMessageDto");
113+
114+
final Method decodeFrom =
115+
dtoClass.getMethod("decodeFrom", DirectBuffer.class, int.class, short.class, short.class);
116+
117+
final Method encodeWith =
118+
dtoClass.getMethod("encodeWithHeaderWith", dtoClass, MutableDirectBuffer.class, int.class);
119+
120+
final int inputLength = encodedMessage.length();
121+
final ExpandableArrayBuffer inputBuffer = encodedMessage.buffer();
122+
final MessageHeaderDecoder header = new MessageHeaderDecoder().wrap(inputBuffer, 0);
123+
final short blockLength = (short)header.blockLength();
124+
final short actingVersion = (short)header.version();
125+
final Object dto = decodeFrom.invoke(null,
126+
encodedMessage.buffer(), MessageHeaderDecoder.ENCODED_LENGTH, blockLength, actingVersion);
127+
outputBuffer.setMemory(0, outputBuffer.capacity(), (byte)0);
128+
final int outputLength = (int)encodeWith.invoke(null, dto, outputBuffer, 0);
129+
if (!areEqual(inputBuffer, inputLength, outputBuffer, outputLength))
130+
{
131+
throw new AssertionError(
132+
"Input and output differ\n\n" +
133+
"SCHEMA:\n" + encodedMessage.schema());
134+
}
135+
136+
success = true;
137+
}
138+
}
139+
finally
140+
{
141+
if (!success)
142+
{
143+
outputManager.dumpSources();
144+
}
145+
}
146+
}
147+
148+
private boolean areEqual(
149+
final ExpandableArrayBuffer inputBuffer,
150+
final int inputLength,
151+
final ExpandableArrayBuffer outputBuffer,
152+
final int outputLength)
153+
{
154+
return new UnsafeBuffer(inputBuffer, 0, inputLength).equals(new UnsafeBuffer(outputBuffer, 0, outputLength));
155+
}
47156

48157
@Property
49158
void csharpDtoEncodeShouldBeTheInverseOfDtoDecode(
@@ -274,4 +383,5 @@ private static void copyResourceToFile(
274383
throw new RuntimeException(e);
275384
}
276385
}
386+
277387
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2013-2024 Real Logic Limited.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package uk.co.real_logic.sbe.properties.utils;
18+
19+
import org.agrona.generation.DynamicPackageOutputManager;
20+
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.StringWriter;
24+
import java.io.Writer;
25+
import java.net.URI;
26+
import java.net.URL;
27+
import java.net.URLClassLoader;
28+
import java.util.*;
29+
import javax.tools.*;
30+
31+
/**
32+
* An implementation of {@link DynamicPackageOutputManager} that stores generated source code in memory and compiles it
33+
* on demand.
34+
*/
35+
public class InMemoryOutputManager implements DynamicPackageOutputManager
36+
{
37+
private final String packageName;
38+
private final Map<String, InMemoryJavaFileObject> sourceFiles = new HashMap<>();
39+
private String packageNameOverride;
40+
41+
public InMemoryOutputManager(final String packageName)
42+
{
43+
this.packageName = packageName;
44+
}
45+
46+
@Override
47+
public Writer createOutput(final String name)
48+
{
49+
return new InMemoryWriter(name);
50+
}
51+
52+
@Override
53+
public void setPackageName(final String packageName)
54+
{
55+
packageNameOverride = packageName;
56+
}
57+
58+
/**
59+
* Compile the generated sources and return a {@link URLClassLoader} that can be used to load the generated classes.
60+
*
61+
* @return a {@link URLClassLoader} that can be used to load the generated classes
62+
*/
63+
public URLClassLoader compileGeneratedSources()
64+
{
65+
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
66+
final StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null);
67+
final InMemoryFileManager fileManager = new InMemoryFileManager(standardFileManager);
68+
final JavaCompiler.CompilationTask task = compiler.getTask(
69+
null,
70+
fileManager,
71+
null,
72+
null,
73+
null,
74+
sourceFiles.values()
75+
);
76+
77+
if (!task.call())
78+
{
79+
throw new IllegalStateException("Compilation failed");
80+
}
81+
82+
final GeneratedCodeLoader classLoader = new GeneratedCodeLoader(getClass().getClassLoader());
83+
classLoader.defineClasses(fileManager);
84+
return classLoader;
85+
}
86+
87+
public void dumpSources()
88+
{
89+
sourceFiles.forEach((qualifiedName, file) ->
90+
{
91+
System.out.println(
92+
System.lineSeparator() + "Source file: " + qualifiedName +
93+
System.lineSeparator() + file.sourceCode +
94+
System.lineSeparator());
95+
});
96+
}
97+
98+
class InMemoryWriter extends StringWriter
99+
{
100+
private final String name;
101+
102+
InMemoryWriter(final String name)
103+
{
104+
this.name = name;
105+
}
106+
107+
@Override
108+
public void close() throws IOException
109+
{
110+
super.close();
111+
final String actingPackageName = packageNameOverride == null ? packageName : packageNameOverride;
112+
packageNameOverride = null;
113+
114+
final String qualifiedName = actingPackageName + "." + name;
115+
final InMemoryJavaFileObject sourceFile =
116+
new InMemoryJavaFileObject(qualifiedName, getBuffer().toString());
117+
118+
final InMemoryJavaFileObject existingFile = sourceFiles.putIfAbsent(qualifiedName, sourceFile);
119+
120+
if (existingFile != null && !Objects.equals(existingFile.sourceCode, sourceFile.sourceCode))
121+
{
122+
throw new IllegalStateException("Duplicate (but different) class: " + qualifiedName);
123+
}
124+
}
125+
}
126+
127+
static class InMemoryFileManager extends ForwardingJavaFileManager<StandardJavaFileManager>
128+
{
129+
private final List<InMemoryJavaFileObject> outputFiles = new ArrayList<>();
130+
131+
InMemoryFileManager(final StandardJavaFileManager fileManager)
132+
{
133+
super(fileManager);
134+
}
135+
136+
@Override
137+
public JavaFileObject getJavaFileForOutput(
138+
final Location location,
139+
final String className,
140+
final JavaFileObject.Kind kind,
141+
final FileObject sibling)
142+
{
143+
final InMemoryJavaFileObject outputFile = new InMemoryJavaFileObject(className, kind);
144+
outputFiles.add(outputFile);
145+
return outputFile;
146+
}
147+
148+
public Collection<InMemoryJavaFileObject> outputFiles()
149+
{
150+
return outputFiles;
151+
}
152+
}
153+
154+
static class InMemoryJavaFileObject extends SimpleJavaFileObject
155+
{
156+
private final String sourceCode;
157+
private final ByteArrayOutputStream outputStream;
158+
159+
InMemoryJavaFileObject(final String className, final String sourceCode)
160+
{
161+
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
162+
this.sourceCode = sourceCode;
163+
this.outputStream = new ByteArrayOutputStream();
164+
}
165+
166+
InMemoryJavaFileObject(final String className, final Kind kind)
167+
{
168+
super(URI.create("mem:///" + className.replace('.', '/') + kind.extension), kind);
169+
this.sourceCode = null;
170+
this.outputStream = new ByteArrayOutputStream();
171+
}
172+
173+
@Override
174+
public CharSequence getCharContent(final boolean ignoreEncodingErrors)
175+
{
176+
return sourceCode;
177+
}
178+
179+
@Override
180+
public ByteArrayOutputStream openOutputStream()
181+
{
182+
return outputStream;
183+
}
184+
185+
public byte[] getClassBytes()
186+
{
187+
return outputStream.toByteArray();
188+
}
189+
}
190+
191+
static class GeneratedCodeLoader extends URLClassLoader
192+
{
193+
GeneratedCodeLoader(final ClassLoader parent)
194+
{
195+
super(new URL[0], parent);
196+
}
197+
198+
void defineClasses(final InMemoryFileManager fileManager)
199+
{
200+
fileManager.outputFiles().forEach(file ->
201+
{
202+
final byte[] classBytes = file.getClassBytes();
203+
super.defineClass(file.getName(), classBytes, 0, classBytes.length);
204+
});
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)