Skip to content

Commit 8257ead

Browse files
authored
Merge pull request #1202 from quickfix-j/copilot/ensure-message-code-generator-consistency
Add golden-file regression tests for FIX42, FIX44, FixLatest code generation
2 parents c61418d + a0ac612 commit 8257ead

1,565 files changed

Lines changed: 434073 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ protected void logInfo(String msg) {
8181
}
8282

8383
protected void logDebug(String msg) {
84-
System.out.println(msg);
84+
// no-op by default; override (e.g. MavenMessageCodeGenerator) to enable debug output
8585
}
8686

8787
protected void logError(String msg, Throwable e) {
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package org.quickfixj.codegenerator;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.fail;
5+
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.stream.Collectors;
13+
import java.util.stream.Stream;
14+
15+
import org.junit.Before;
16+
import org.junit.Rule;
17+
import org.junit.Test;
18+
import org.junit.rules.TemporaryFolder;
19+
20+
/**
21+
* Golden-file regression test for the code generator.
22+
*
23+
* <p>The test runs {@link MessageCodeGenerator} against the real FIX42 and FIX44 dictionaries
24+
* and compares every generated {@code .java} file byte-for-byte against the committed golden
25+
* files stored in {@code src/test/resources/golden/fix42} and
26+
* {@code src/test/resources/golden/fix44}.
27+
*
28+
* <p>FIX42 is used because it contains repeating groups (38 of them), while still being
29+
* smaller than FIX44. FIX44 is used because it contains 233 groups, including nested ones
30+
* (relevant to issue #1084).
31+
*
32+
* <p>If the generator output must intentionally change, regenerate the golden files by running
33+
* the {@code GenerateGoldenFiles} utility in the {@code src/test/java} tree and committing the
34+
* updated golden files together with the generator changes.
35+
*/
36+
public class GoldenFileTest {
37+
38+
@Rule
39+
public TemporaryFolder tempFolder = new TemporaryFolder();
40+
41+
private File schemaDirectory = new File("./src/main/resources/org/quickfixj/codegenerator");
42+
private File goldenBase = new File("./src/test/resources/golden");
43+
44+
private File fix42DictFile = new File(
45+
"../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml");
46+
private File fix44DictFile = new File(
47+
"../quickfixj-messages/quickfixj-messages-fix44/src/main/resources/FIX44.xml");
48+
49+
private MessageCodeGenerator generator;
50+
51+
@Before
52+
public void setup() {
53+
generator = new MessageCodeGenerator();
54+
}
55+
56+
// -------------------------------------------------------------------------
57+
// FIX42 – has fields, messages, and repeating groups (no components)
58+
// -------------------------------------------------------------------------
59+
60+
@Test
61+
public void testFix42GenerationMatchesGolden() throws Exception {
62+
File outputDir = tempFolder.newFolder("fix42");
63+
generateCode(fix42DictFile, "FIX42", "quickfix.fix42", outputDir);
64+
assertMatchesGolden(new File(goldenBase, "fix42"), outputDir, "FIX42");
65+
}
66+
67+
// -------------------------------------------------------------------------
68+
// FIX44 – has fields, messages, components, and 233 groups (incl. nested)
69+
// -------------------------------------------------------------------------
70+
71+
@Test
72+
public void testFix44GenerationMatchesGolden() throws Exception {
73+
File outputDir = tempFolder.newFolder("fix44");
74+
generateCode(fix44DictFile, "FIX44", "quickfix.fix44", outputDir);
75+
assertMatchesGolden(new File(goldenBase, "fix44"), outputDir, "FIX44");
76+
}
77+
78+
// -------------------------------------------------------------------------
79+
// Helpers
80+
// -------------------------------------------------------------------------
81+
82+
private void generateCode(File dictFile, String name, String messagePackage, File outputDir)
83+
throws Exception {
84+
MessageCodeGenerator.Task task = new MessageCodeGenerator.Task();
85+
task.setName(name);
86+
task.setSpecification(dictFile);
87+
task.setTransformDirectory(schemaDirectory);
88+
task.setMessagePackage(messagePackage);
89+
task.setOutputBaseDirectory(outputDir);
90+
task.setFieldPackage("quickfix.field");
91+
task.setOverwrite(true);
92+
task.setOrderedFields(true);
93+
task.setDecimalGenerated(true);
94+
generator.generate(task);
95+
}
96+
97+
/**
98+
* Recursively walks the golden directory and verifies that each {@code .java} file has an
99+
* identical counterpart in the generated output directory. Also checks that no extra files
100+
* were generated that are absent from the golden directory.
101+
*/
102+
private void assertMatchesGolden(File goldenDir, File generatedDir, String label)
103+
throws IOException {
104+
List<String> errors = new ArrayList<>();
105+
106+
// Collect relative paths of all .java files in the golden directory
107+
List<String> goldenRelPaths = collectJavaPaths(goldenDir.toPath());
108+
109+
// Collect relative paths of all .java files in the generated output directory
110+
List<String> generatedRelPaths = collectJavaPaths(generatedDir.toPath());
111+
112+
// Files present in golden but missing from generated output
113+
List<String> missingFromGenerated = new ArrayList<>(goldenRelPaths);
114+
missingFromGenerated.removeAll(generatedRelPaths);
115+
for (String missing : missingFromGenerated) {
116+
errors.add("[" + label + "] Missing generated file: " + missing);
117+
}
118+
119+
// Extra files in generated output that are not in golden
120+
List<String> extraInGenerated = new ArrayList<>(generatedRelPaths);
121+
extraInGenerated.removeAll(goldenRelPaths);
122+
for (String extra : extraInGenerated) {
123+
errors.add("[" + label + "] Unexpected generated file (not in golden): " + extra);
124+
}
125+
126+
// Compare content of files present in both
127+
for (String relPath : goldenRelPaths) {
128+
if (!generatedRelPaths.contains(relPath)) {
129+
continue; // already reported as missing above
130+
}
131+
File goldenFile = new File(goldenDir, relPath);
132+
File generatedFile = new File(generatedDir, relPath);
133+
compareFileContent(goldenFile, generatedFile, relPath, label, errors);
134+
}
135+
136+
if (!errors.isEmpty()) {
137+
fail(errors.size() + " golden file assertion(s) failed:\n"
138+
+ String.join("\n", errors));
139+
}
140+
}
141+
142+
private List<String> collectJavaPaths(Path root) throws IOException {
143+
if (!root.toFile().exists()) {
144+
return new ArrayList<>();
145+
}
146+
try (Stream<Path> stream = Files.walk(root)) {
147+
return stream
148+
.filter(p -> p.toString().endsWith(".java"))
149+
.map(p -> root.relativize(p).toString())
150+
.sorted()
151+
.collect(Collectors.toList());
152+
}
153+
}
154+
155+
private void compareFileContent(File goldenFile, File generatedFile, String relPath,
156+
String label, List<String> errors) throws IOException {
157+
List<String> goldenLines = Files.readAllLines(goldenFile.toPath());
158+
List<String> generatedLines = Files.readAllLines(generatedFile.toPath());
159+
160+
int maxLines = Math.max(goldenLines.size(), generatedLines.size());
161+
for (int i = 0; i < maxLines; i++) {
162+
String goldenLine = i < goldenLines.size() ? goldenLines.get(i) : "<EOF>";
163+
String generatedLine = i < generatedLines.size() ? generatedLines.get(i) : "<EOF>";
164+
if (!goldenLine.equals(generatedLine)) {
165+
errors.add(String.format(
166+
"[%s] %s line %d differs:%n golden: %s%n generated: %s",
167+
label, relPath, i + 1, goldenLine, generatedLine));
168+
// Report only the first differing line per file to keep output manageable
169+
break;
170+
}
171+
}
172+
if (goldenLines.size() != generatedLines.size() && errors.isEmpty()) {
173+
// Line counts differ but all shared lines matched – report the length mismatch
174+
errors.add(String.format(
175+
"[%s] %s line count differs: golden=%d, generated=%d",
176+
label, relPath, goldenLines.size(), generatedLines.size()));
177+
}
178+
}
179+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Golden Files for Code Generator Regression Tests
2+
3+
This directory contains the reference ("golden") output of `MessageCodeGenerator`
4+
for **FIX42** and **FIX44**. They are used by `GoldenFileTest` to catch
5+
unintended changes to generated code.
6+
7+
## Directory layout
8+
9+
```
10+
golden/
11+
fix42/ – output generated from quickfixj-messages-fix42/src/main/resources/FIX42.xml
12+
fix44/ – output generated from quickfixj-messages-fix44/src/main/resources/FIX44.xml
13+
```
14+
15+
## What the test does
16+
17+
`GoldenFileTest` runs `MessageCodeGenerator` against both dictionaries into a
18+
temporary folder, then walks every `.java` file and asserts line-by-line equality
19+
with the corresponding file here. Missing or extra files also fail the test.
20+
21+
## When the generator output changes intentionally
22+
23+
1. Make your generator changes.
24+
2. Rebuild the module to pick up the new code:
25+
```bash
26+
./mvnw package -pl quickfixj-codegenerator -DskipTests
27+
```
28+
3. Regenerate the golden files by running the generator against both dictionaries
29+
from the `quickfixj-codegenerator` directory:
30+
```bash
31+
# FIX42
32+
java -cp "target/quickfixj-codegenerator-*-SNAPSHOT.jar:$(./mvnw -q dependency:build-classpath -DincludeScope=compile -Dmdep.outputFile=/dev/stdout)" \
33+
org.quickfixj.codegenerator.MessageCodeGenerator \
34+
--spec ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml \
35+
--transform src/main/resources/org/quickfixj/codegenerator \
36+
--out src/test/resources/golden/fix42 \
37+
--messagePackage quickfix.fix42 --fieldPackage quickfix.field \
38+
--orderedFields --decimal
39+
```
40+
The easiest way is to use the existing `GoldenFileTest` parameters as a guide
41+
and write a small standalone `main` — or simply copy the generated output from
42+
the temporary folder that `GoldenFileTest` creates (set a breakpoint, or change
43+
`tempFolder` to a fixed path temporarily).
44+
45+
Alternatively, run the following Maven snippet from the repo root, which uses
46+
the same settings as the test:
47+
```bash
48+
./mvnw test -pl quickfixj-codegenerator -Dtest=GenerateGoldenFilesManual
49+
```
50+
*(Create a one-off test class that calls the generator and copies output to
51+
`src/test/resources/golden/` if you prefer a scripted approach.)*
52+
53+
4. Verify only the expected files changed:
54+
```bash
55+
git diff --stat quickfixj-codegenerator/src/test/resources/golden/
56+
```
57+
5. Run the full test suite to confirm the updated golden files now match:
58+
```bash
59+
./mvnw test -pl quickfixj-codegenerator
60+
```
61+
6. Commit the updated golden files **together with your generator changes** in the
62+
same commit (or PR) so reviewers can see the diff side-by-side.
63+
64+
## Why FIX42 and FIX44?
65+
66+
| Coverage area | FIX42 | FIX44 |
67+
|------------------------|-------|-------|
68+
| Fields |||
69+
| Messages |||
70+
| Components | ||
71+
| Repeating groups | ✓ (38)| ✓ (233, incl. nested) |
72+
| Message cracker/factory|||
73+
74+
FIX42 is small enough to keep test times short while still exercising the
75+
group-generation path. FIX44's 233 groups (including nested groups relevant to
76+
issue #1084) provide thorough coverage without needing all FIX versions.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Generated Java Source File */
2+
/*******************************************************************************
3+
* Copyright (c) quickfixengine.org All rights reserved.
4+
*
5+
* This file is part of the QuickFIX FIX Engine
6+
*
7+
* This file may be distributed under the terms of the quickfixengine.org
8+
* license as defined by quickfixengine.org and appearing in the file
9+
* LICENSE included in the packaging of this file.
10+
*
11+
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
12+
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
13+
* PARTICULAR PURPOSE.
14+
*
15+
* See http://www.quickfixengine.org/LICENSE for licensing information.
16+
*
17+
* Contact ask@quickfixengine.org if any conditions of this licensing
18+
* are not clear to you.
19+
******************************************************************************/
20+
21+
package quickfix.field;
22+
23+
import quickfix.StringField;
24+
25+
public class Account extends StringField {
26+
27+
static final long serialVersionUID = 20050617;
28+
29+
public static final int FIELD = 1;
30+
31+
public Account() {
32+
super(1);
33+
}
34+
35+
public Account(String data) {
36+
super(1, data);
37+
}
38+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* Generated Java Source File */
2+
/*******************************************************************************
3+
* Copyright (c) quickfixengine.org All rights reserved.
4+
*
5+
* This file is part of the QuickFIX FIX Engine
6+
*
7+
* This file may be distributed under the terms of the quickfixengine.org
8+
* license as defined by quickfixengine.org and appearing in the file
9+
* LICENSE included in the packaging of this file.
10+
*
11+
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
12+
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
13+
* PARTICULAR PURPOSE.
14+
*
15+
* See http://www.quickfixengine.org/LICENSE for licensing information.
16+
*
17+
* Contact ask@quickfixengine.org if any conditions of this licensing
18+
* are not clear to you.
19+
******************************************************************************/
20+
21+
package quickfix.field;
22+
23+
import quickfix.DecimalField;
24+
25+
public class AccruedInterestAmt extends DecimalField {
26+
27+
static final long serialVersionUID = 20050617;
28+
29+
public static final int FIELD = 159;
30+
31+
public AccruedInterestAmt() {
32+
super(159);
33+
}
34+
35+
public AccruedInterestAmt(java.math.BigDecimal data) {
36+
super(159, data);
37+
}
38+
39+
public AccruedInterestAmt(double data) {
40+
super(159, new java.math.BigDecimal(data));
41+
}
42+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Generated Java Source File */
2+
/*******************************************************************************
3+
* Copyright (c) quickfixengine.org All rights reserved.
4+
*
5+
* This file is part of the QuickFIX FIX Engine
6+
*
7+
* This file may be distributed under the terms of the quickfixengine.org
8+
* license as defined by quickfixengine.org and appearing in the file
9+
* LICENSE included in the packaging of this file.
10+
*
11+
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
12+
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
13+
* PARTICULAR PURPOSE.
14+
*
15+
* See http://www.quickfixengine.org/LICENSE for licensing information.
16+
*
17+
* Contact ask@quickfixengine.org if any conditions of this licensing
18+
* are not clear to you.
19+
******************************************************************************/
20+
21+
package quickfix.field;
22+
23+
import quickfix.DoubleField;
24+
25+
public class AccruedInterestRate extends DoubleField {
26+
27+
static final long serialVersionUID = 20050617;
28+
29+
public static final int FIELD = 158;
30+
31+
public AccruedInterestRate() {
32+
super(158);
33+
}
34+
35+
public AccruedInterestRate(double data) {
36+
super(158, data);
37+
}
38+
}

0 commit comments

Comments
 (0)