-
Notifications
You must be signed in to change notification settings - Fork 29
Make the determinism failure say what differed #1245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8416af0
b5ce109
d5ca85a
833023c
d8d3f1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package tests.wurstscript.tests; | ||
|
|
||
| import org.testng.annotations.Test; | ||
|
|
||
| import static org.testng.Assert.assertFalse; | ||
| import static org.testng.Assert.assertNotEquals; | ||
| import static org.testng.Assert.assertTrue; | ||
|
|
||
| /** | ||
| * Covers the summary the determinism test prints when it fails. | ||
| * <p> | ||
| * The failure it describes has happened once and is not reproducible on demand, so the summary is | ||
| * the only thing that will be read when it happens again. That makes it worth testing on its own: | ||
| * a description which misreports what changed is worse than none, because it sends the next person | ||
| * looking in the wrong place. | ||
| */ | ||
| public class DeterminismDiffReportTest { | ||
|
|
||
| /** | ||
| * A block appearing in one script and not the other reads as one addition. Comparing equal | ||
| * indexes instead would report every line after the insertion as differing, which is the case | ||
| * emission-order nondeterminism actually produces. | ||
| */ | ||
| @Test | ||
| public void anInsertedBlockIsOneAdditionRatherThanEverythingAfterIt() { | ||
| String first = "a\nb\nc\nd\ne\nf\ng\nh\n"; | ||
| String second = "a\nb\nINSERTED\nc\nd\ne\nf\ng\nh\n"; | ||
|
|
||
| String report = LuaTranslationTests.describeFirstDifferences(first, second); | ||
|
|
||
| assertTrue(report.contains("0 line(s) only in the first, 1 only in the second"), | ||
| "one inserted line should read as one addition:\n" + report); | ||
| assertTrue(report.contains("INSERTED"), "the added line should be named:\n" + report); | ||
| assertTrue(report.contains("line 3"), "the added line's number should be given:\n" + report); | ||
| } | ||
|
|
||
| /** A block moved rather than inserted reads as one removal and one addition, not a cascade. */ | ||
| @Test | ||
| public void aMovedLineIsOneRemovalAndOneAddition() { | ||
| String first = "one\nmoved\ntwo\nthree\nfour\n"; | ||
| String second = "one\ntwo\nthree\nmoved\nfour\n"; | ||
|
|
||
| String report = LuaTranslationTests.describeFirstDifferences(first, second); | ||
|
|
||
| assertTrue(report.contains("1 line(s) only in the first, 1 only in the second"), | ||
| "a moved line should read as one removal and one addition:\n" + report); | ||
| } | ||
|
|
||
| /** Replacing a line in place is a removal and an addition at the same position. */ | ||
| @Test | ||
| public void aReplacedLineNamesBothVersions() { | ||
| String first = "x\nbefore\nz\n"; | ||
| String second = "x\nafter\nz\n"; | ||
|
|
||
| String report = LuaTranslationTests.describeFirstDifferences(first, second); | ||
|
|
||
| assertTrue(report.contains("before"), "the first version should be named:\n" + report); | ||
| assertTrue(report.contains("after"), "the second version should be named:\n" + report); | ||
| } | ||
|
|
||
| /** | ||
| * Two scripts whose lines all match but which are not equal differ in how the lines end. The | ||
| * caller only asks after finding them unequal, so passing identical strings would test a state | ||
| * production never reaches — this passes CRLF against LF, which it can. | ||
| */ | ||
| @Test | ||
| public void differingOnlyInLineEndingsSaysSoRatherThanListingEveryLine() { | ||
| String crlf = "alpha\r\nbeta\r\ngamma\r\n"; | ||
| String lf = "alpha\nbeta\ngamma\n"; | ||
| assertNotEquals(crlf, lf, "the two inputs must be unequal for this to mean anything"); | ||
|
|
||
| String report = LuaTranslationTests.describeFirstDifferences(crlf, lf); | ||
|
|
||
| assertTrue(report.contains("line terminators"), | ||
| "line endings should be named rather than every line reported as changed:\n" + report); | ||
| assertFalse(report.contains("only in the first"), | ||
| "no line should be reported as removed:\n" + report); | ||
| } | ||
|
|
||
| /** Bytes after the last line reach the same branch: every line matches, the scripts do not. */ | ||
| @Test | ||
| public void trailingBytesReachTheSameCase() { | ||
| String report = LuaTranslationTests.describeFirstDifferences("a\nb\n", "a\nb"); | ||
|
|
||
| assertTrue(report.contains("line terminators") || report.contains("only in the"), | ||
| "a trailing difference should be described one way or the other:\n" + report); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1418,7 +1418,102 @@ public void luaOutputIsDeterministicForGenericOverrideSlots() throws IOException | |
| test().testLua(true).compilationUnits(genericOverrideReproUnits()); | ||
| String second = Files.toString(new File("test-output/lua/LuaTranslationTests_luaOutputIsDeterministicForGenericOverrideSlots.lua"), Charsets.UTF_8); | ||
|
|
||
| assertEquals(first, second); | ||
| if (!first.equals(second)) { | ||
| // This has failed once on CI and not since, and 250 compiles in one JVM did not | ||
| // reproduce it. A bare "expected X but got Y" over two whole scripts is unreadable and | ||
| // the run's output is gone by the time anyone looks, so the failure carries what it | ||
| // takes to act on: both scripts kept beside the test output, and the differing lines | ||
| // named. Without this the next occurrence is as unactionable as the first. | ||
| File firstFile = new File(TEST_OUTPUT_PATH, "determinism-first.lua"); | ||
| File secondFile = new File(TEST_OUTPUT_PATH, "determinism-second.lua"); | ||
| Files.write(first.getBytes(Charsets.UTF_8), firstFile); | ||
| Files.write(second.getBytes(Charsets.UTF_8), secondFile); | ||
| fail("the same program compiled to different Lua twice in one run." | ||
| + "\n" + describeFirstDifferences(first, second) | ||
| + "\nboth kept at " + firstFile.getPath() + " and " + secondFile.getPath()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * What changed between the two scripts, aligned rather than compared line by line. | ||
| * <p> | ||
| * Comparing equal indexes is not a diff: emission order moving a block shifts every line after | ||
| * it, so the count becomes "everything from here down" and the listed pairs are unrelated. That | ||
| * is the shape this diagnostic exists to investigate, so it is the shape it has to describe. | ||
| * Aligned on the longest common subsequence, a moved block reads as one removal and one addition. | ||
| * <p> | ||
| * The scripts are also uploaded as an artifact on a failing CI run, but the message has to stand | ||
| * on its own: an artifact needs fetching, and the check is what gets read first. Bounded so a | ||
| * wholesale difference does not bury the report, with the totals stated either way. | ||
| */ | ||
| /** | ||
| * Splits on either terminator, so a CRLF script aligns against an LF one line for line. Splitting | ||
| * on "\n" alone leaves the carriage return in the line text, which makes every line of a CRLF | ||
| * script differ from its LF counterpart and buries the actual difference. | ||
| */ | ||
| private static final String NEWLINE_RE = "\r?\n"; | ||
| private static final char NEWLINE = '\n'; | ||
|
|
||
| static String describeFirstDifferences(String first, String second) { | ||
| final int reportLimit = 40; | ||
| String[] a = first.split(NEWLINE_RE, -1); | ||
| String[] b = second.split(NEWLINE_RE, -1); | ||
|
Comment on lines
+1459
to
+1460
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because AGENTS.md reference: AGENTS.md:L82-L84 Useful? React with 👍 / 👎. |
||
|
|
||
| // O(n*m) in memory, so a pathological pair falls back to reporting the sizes rather than | ||
| // exhausting the worker. The scripts this compares are a few hundred lines. | ||
| if ((long) a.length * b.length > 4_000_000L) { | ||
| return " too large to align: " + a.length + " lines against " + b.length; | ||
| } | ||
|
|
||
| int[][] common = new int[a.length + 1][b.length + 1]; | ||
| for (int i = a.length - 1; i >= 0; i--) { | ||
| for (int j = b.length - 1; j >= 0; j--) { | ||
| common[i][j] = a[i].equals(b[j]) | ||
| ? common[i + 1][j + 1] + 1 | ||
| : Math.max(common[i + 1][j], common[i][j + 1]); | ||
| } | ||
| } | ||
|
|
||
| List<String> entries = new ArrayList<>(); | ||
| int removed = 0; | ||
| int added = 0; | ||
| int i = 0; | ||
| int j = 0; | ||
| while (i < a.length || j < b.length) { | ||
| if (i < a.length && j < b.length && a[i].equals(b[j])) { | ||
| i++; | ||
| j++; | ||
| } else if (j >= b.length || (i < a.length && common[i + 1][j] >= common[i][j + 1])) { | ||
| removed++; | ||
| if (entries.size() < reportLimit) { | ||
| entries.add(" only in first, line " + (i + 1) + ": " + a[i]); | ||
| } | ||
| i++; | ||
| } else { | ||
| added++; | ||
| if (entries.size() < reportLimit) { | ||
| entries.add(" only in second, line " + (j + 1) + ": " + b[j]); | ||
| } | ||
| j++; | ||
| } | ||
| } | ||
|
|
||
| if (removed == 0 && added == 0) { | ||
| // Reached because the split accepts either terminator: two scripts whose lines all match | ||
| // and which are still unequal differ in how those lines end, or in bytes after the last | ||
| // one. Worth saying plainly rather than reporting every line as changed, which is what | ||
| // splitting on "\n" alone did - it left the carriage returns in the line text. | ||
| return " every line matches, so the two differ only in line terminators or trailing" | ||
| + " bytes: " + first.length() + " characters against " + second.length(); | ||
| } | ||
| StringBuilder sb = new StringBuilder(); | ||
| sb.append(" ").append(removed).append(" line(s) only in the first, ") | ||
| .append(added).append(" only in the second"); | ||
| sb.append(removed + added > entries.size() ? ", first " + entries.size() + ":" + NEWLINE : ":" + NEWLINE); | ||
| for (String entry : entries) { | ||
| sb.append(entry).append(NEWLINE); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
|
|
||
| @Test | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this intermittent test fails in CI, these files are written only to the ephemeral runner filesystem, so the promised full evidence is still gone after the job finishes. The checked workflow uploads only release archives in
.github/workflows/build.ymllines 130–137—and that step does not useif: always()—while the JUnit report retains only the five-line summary. Add a failure-time artifact upload for these scripts, or include the complete diff in the test result.Useful? React with 👍 / 👎.