Skip to content
Draft
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected void logInfo(String msg) {
}

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

protected void logError(String msg, Throwable e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package org.quickfixj.codegenerator;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

/**
* Golden-file regression test for the code generator.
*
* <p>The test runs {@link MessageCodeGenerator} against the real FIX42 and FIX44 dictionaries
* and compares every generated {@code .java} file byte-for-byte against the committed golden
* files stored in {@code src/test/resources/golden/fix42} and
* {@code src/test/resources/golden/fix44}.
*
* <p>FIX42 is used because it contains repeating groups (38 of them), while still being
* smaller than FIX44. FIX44 is used because it contains 233 groups, including nested ones
* (relevant to issue #1084).
*
* <p>If the generator output must intentionally change, regenerate the golden files by running
* the {@code GenerateGoldenFiles} utility in the {@code src/test/java} tree and committing the
* updated golden files together with the generator changes.
*/
public class GoldenFileTest {

@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();

private File schemaDirectory = new File("./src/main/resources/org/quickfixj/codegenerator");
private File goldenBase = new File("./src/test/resources/golden");

private File fix42DictFile = new File(
"../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml");
private File fix44DictFile = new File(
"../quickfixj-messages/quickfixj-messages-fix44/src/main/resources/FIX44.xml");

private MessageCodeGenerator generator;

@Before
public void setup() {
generator = new MessageCodeGenerator();
}

// -------------------------------------------------------------------------
// FIX42 – has fields, messages, and repeating groups (no components)
// -------------------------------------------------------------------------

@Test
public void testFix42GenerationMatchesGolden() throws Exception {
File outputDir = tempFolder.newFolder("fix42");
generateCode(fix42DictFile, "FIX42", "quickfix.fix42", outputDir);
assertMatchesGolden(new File(goldenBase, "fix42"), outputDir, "FIX42");
}

// -------------------------------------------------------------------------
// FIX44 – has fields, messages, components, and 233 groups (incl. nested)
// -------------------------------------------------------------------------

@Test
public void testFix44GenerationMatchesGolden() throws Exception {
File outputDir = tempFolder.newFolder("fix44");
generateCode(fix44DictFile, "FIX44", "quickfix.fix44", outputDir);
assertMatchesGolden(new File(goldenBase, "fix44"), outputDir, "FIX44");
}

// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------

private void generateCode(File dictFile, String name, String messagePackage, File outputDir)
throws Exception {
MessageCodeGenerator.Task task = new MessageCodeGenerator.Task();
task.setName(name);
task.setSpecification(dictFile);
task.setTransformDirectory(schemaDirectory);
task.setMessagePackage(messagePackage);
task.setOutputBaseDirectory(outputDir);
task.setFieldPackage("quickfix.field");
task.setOverwrite(true);
task.setOrderedFields(true);
task.setDecimalGenerated(true);
generator.generate(task);
}

/**
* Recursively walks the golden directory and verifies that each {@code .java} file has an
* identical counterpart in the generated output directory. Also checks that no extra files
* were generated that are absent from the golden directory.
*/
private void assertMatchesGolden(File goldenDir, File generatedDir, String label)
throws IOException {
List<String> errors = new ArrayList<>();

// Collect relative paths of all .java files in the golden directory
List<String> goldenRelPaths = collectJavaPaths(goldenDir.toPath());

// Collect relative paths of all .java files in the generated output directory
List<String> generatedRelPaths = collectJavaPaths(generatedDir.toPath());

// Files present in golden but missing from generated output
List<String> missingFromGenerated = new ArrayList<>(goldenRelPaths);
missingFromGenerated.removeAll(generatedRelPaths);
for (String missing : missingFromGenerated) {
errors.add("[" + label + "] Missing generated file: " + missing);
}

// Extra files in generated output that are not in golden
List<String> extraInGenerated = new ArrayList<>(generatedRelPaths);
extraInGenerated.removeAll(goldenRelPaths);
for (String extra : extraInGenerated) {
errors.add("[" + label + "] Unexpected generated file (not in golden): " + extra);
}

// Compare content of files present in both
for (String relPath : goldenRelPaths) {
if (!generatedRelPaths.contains(relPath)) {
continue; // already reported as missing above
}
File goldenFile = new File(goldenDir, relPath);
File generatedFile = new File(generatedDir, relPath);
compareFileContent(goldenFile, generatedFile, relPath, label, errors);
}

if (!errors.isEmpty()) {
fail(errors.size() + " golden file assertion(s) failed:\n"
+ String.join("\n", errors));
}
}

private List<String> collectJavaPaths(Path root) throws IOException {
if (!root.toFile().exists()) {
return new ArrayList<>();
}
try (Stream<Path> stream = Files.walk(root)) {
return stream
.filter(p -> p.toString().endsWith(".java"))
.map(p -> root.relativize(p).toString())
.sorted()
.collect(Collectors.toList());
}
}

private void compareFileContent(File goldenFile, File generatedFile, String relPath,
String label, List<String> errors) throws IOException {
List<String> goldenLines = Files.readAllLines(goldenFile.toPath());
List<String> generatedLines = Files.readAllLines(generatedFile.toPath());

int maxLines = Math.max(goldenLines.size(), generatedLines.size());
for (int i = 0; i < maxLines; i++) {
String goldenLine = i < goldenLines.size() ? goldenLines.get(i) : "<EOF>";
String generatedLine = i < generatedLines.size() ? generatedLines.get(i) : "<EOF>";
if (!goldenLine.equals(generatedLine)) {
errors.add(String.format(
"[%s] %s line %d differs:%n golden: %s%n generated: %s",
label, relPath, i + 1, goldenLine, generatedLine));
// Report only the first differing line per file to keep output manageable
break;
}
}
if (goldenLines.size() != generatedLines.size() && errors.isEmpty()) {
// Line counts differ but all shared lines matched – report the length mismatch
errors.add(String.format(
"[%s] %s line count differs: golden=%d, generated=%d",
label, relPath, goldenLines.size(), generatedLines.size()));
}
}
}
76 changes: 76 additions & 0 deletions quickfixj-codegenerator/src/test/resources/golden/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Golden Files for Code Generator Regression Tests

This directory contains the reference ("golden") output of `MessageCodeGenerator`
for **FIX42** and **FIX44**. They are used by `GoldenFileTest` to catch
unintended changes to generated code.

## Directory layout

```
golden/
fix42/ – output generated from quickfixj-messages-fix42/src/main/resources/FIX42.xml
fix44/ – output generated from quickfixj-messages-fix44/src/main/resources/FIX44.xml
```

## What the test does

`GoldenFileTest` runs `MessageCodeGenerator` against both dictionaries into a
temporary folder, then walks every `.java` file and asserts line-by-line equality
with the corresponding file here. Missing or extra files also fail the test.

## When the generator output changes intentionally

1. Make your generator changes.
2. Rebuild the module to pick up the new code:
```bash
mvn package -pl quickfixj-codegenerator -DskipTests
```
3. Regenerate the golden files by running the generator against both dictionaries
from the `quickfixj-codegenerator` directory:
```bash
# FIX42
java -cp "target/quickfixj-codegenerator-*-SNAPSHOT.jar:$(mvn -q dependency:build-classpath -DincludeScope=compile -Dmdep.outputFile=/dev/stdout)" \
org.quickfixj.codegenerator.MessageCodeGenerator \
--spec ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml \
--transform src/main/resources/org/quickfixj/codegenerator \
--out src/test/resources/golden/fix42 \
--messagePackage quickfix.fix42 --fieldPackage quickfix.field \
--orderedFields --decimal
```
The easiest way is to use the existing `GoldenFileTest` parameters as a guide
and write a small standalone `main` — or simply copy the generated output from
the temporary folder that `GoldenFileTest` creates (set a breakpoint, or change
`tempFolder` to a fixed path temporarily).

Alternatively, run the following Maven snippet from the repo root, which uses
the same settings as the test:
```bash
mvn test -pl quickfixj-codegenerator -Dtest=GenerateGoldenFilesManual
```
*(Create a one-off test class that calls the generator and copies output to
`src/test/resources/golden/` if you prefer a scripted approach.)*

4. Verify only the expected files changed:
```bash
git diff --stat quickfixj-codegenerator/src/test/resources/golden/
```
5. Run the full test suite to confirm the updated golden files now match:
```bash
mvn test -pl quickfixj-codegenerator
```
6. Commit the updated golden files **together with your generator changes** in the
same commit (or PR) so reviewers can see the diff side-by-side.

## Why FIX42 and FIX44?

| Coverage area | FIX42 | FIX44 |
|------------------------|-------|-------|
| Fields | ✓ | ✓ |
| Messages | ✓ | ✓ |
| Components | | ✓ |
| Repeating groups | ✓ (38)| ✓ (233, incl. nested) |
| Message cracker/factory| ✓ | ✓ |

FIX42 is small enough to keep test times short while still exercising the
group-generation path. FIX44's 233 groups (including nested groups relevant to
issue #1084) provide thorough coverage without needing all FIX versions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Generated Java Source File */
/*******************************************************************************
* Copyright (c) quickfixengine.org All rights reserved.
*
* This file is part of the QuickFIX FIX Engine
*
* This file may be distributed under the terms of the quickfixengine.org
* license as defined by quickfixengine.org and appearing in the file
* LICENSE included in the packaging of this file.
*
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE.
*
* See http://www.quickfixengine.org/LICENSE for licensing information.
*
* Contact ask@quickfixengine.org if any conditions of this licensing
* are not clear to you.
******************************************************************************/

package quickfix.field;

import quickfix.StringField;

public class Account extends StringField {

static final long serialVersionUID = 20050617;

public static final int FIELD = 1;

public Account() {
super(1);
}

public Account(String data) {
super(1, data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Generated Java Source File */
/*******************************************************************************
* Copyright (c) quickfixengine.org All rights reserved.
*
* This file is part of the QuickFIX FIX Engine
*
* This file may be distributed under the terms of the quickfixengine.org
* license as defined by quickfixengine.org and appearing in the file
* LICENSE included in the packaging of this file.
*
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE.
*
* See http://www.quickfixengine.org/LICENSE for licensing information.
*
* Contact ask@quickfixengine.org if any conditions of this licensing
* are not clear to you.
******************************************************************************/

package quickfix.field;

import quickfix.DecimalField;

public class AccruedInterestAmt extends DecimalField {

static final long serialVersionUID = 20050617;

public static final int FIELD = 159;

public AccruedInterestAmt() {
super(159);
}

public AccruedInterestAmt(java.math.BigDecimal data) {
super(159, data);
}

public AccruedInterestAmt(double data) {
super(159, new java.math.BigDecimal(data));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* Generated Java Source File */
/*******************************************************************************
* Copyright (c) quickfixengine.org All rights reserved.
*
* This file is part of the QuickFIX FIX Engine
*
* This file may be distributed under the terms of the quickfixengine.org
* license as defined by quickfixengine.org and appearing in the file
* LICENSE included in the packaging of this file.
*
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE.
*
* See http://www.quickfixengine.org/LICENSE for licensing information.
*
* Contact ask@quickfixengine.org if any conditions of this licensing
* are not clear to you.
******************************************************************************/

package quickfix.field;

import quickfix.DoubleField;

public class AccruedInterestRate extends DoubleField {

static final long serialVersionUID = 20050617;

public static final int FIELD = 158;

public AccruedInterestRate() {
super(158);
}

public AccruedInterestRate(double data) {
super(158, data);
}
}
Loading
Loading