Skip to content

Commit e954c10

Browse files
committed
Factor out unexpected-error reproduction into dedicated UnexpectedErrorReproducer
1 parent 29291c7 commit e954c10

5 files changed

Lines changed: 190 additions & 102 deletions

File tree

src/sqlancer/common/oracle/AbstractComparisonReproducer.java

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,28 @@
77

88
/**
99
* Shared skeleton for the {@link Reproducer}s of oracles that detect a bug by comparing two evaluations of a
10-
* semantically-equivalent pair (e.g. {@link EETOracle}, {@link NoRECOracle}, {@link TLPWhereOracle}. All of these
11-
* reduce the bug the same way: re-evaluate both sides against the reduced database and report whether they still
12-
* disagree (or, when the original bug was an unexpected DBMS error, whether that same error still fires).
10+
* semantically-equivalent pair (e.g. {@link EETOracle}, {@link NoRECOracle}, {@link TLPWhereOracle}). Reduction re-runs
11+
* both sides against the reduced database and reports whether they still disagree.
1312
*
1413
* <p>
15-
* This class owns that control flow (including distinguishing a still-reproducing error from an unrelated one
16-
* introduced by the reduction) and the {@link #getBugInformation()} header. Subclasses supply the parts specific to
17-
* their oracle: how each side is evaluated, how the two are compared, and how the failing queries are rendered in the
18-
* reduced test case.
14+
* The separate case where the original bug was an unexpected DBMS error rather than a mismatch is handled by
15+
* {@link UnexpectedErrorReproducer}, so a subclass here deals only with comparing two sides and never with error
16+
* handling.
17+
*
18+
* <p>
19+
* This class owns the compare-and-report control flow and the {@link #getBugInformation()} header. Subclasses supply
20+
* how each side is evaluated, how the two are compared, and how the failing queries are rendered in the reduced test
21+
* case.
1922
*
2023
* @param <G>
2124
* the DBMS-specific global state class
2225
* @param <R>
23-
* the type each side evaluates to (e.g. a result set as a list of strings, a row count, a post-image)
26+
* the type each side evaluates to (e.g. a result set as a list of strings, a row count)
2427
*/
2528
public abstract class AbstractComparisonReproducer<G extends SQLGlobalState<?, ?>, R> implements Reproducer<G> {
2629

2730
/**
28-
* The message of the unexpected DBMS error the original bug was, or {@code null} if the original bug was a
29-
* comparison mismatch rather than an error.
30-
*/
31-
protected final String expectedErrorMessage;
32-
33-
protected AbstractComparisonReproducer(String expectedErrorMessage) {
34-
this.expectedErrorMessage = expectedErrorMessage;
35-
}
36-
37-
/**
38-
* Whether the recorded bug has a transformed (second) side. It does not when the bug was a DBMS error triggered by
39-
* the original query alone, in which case there is no second side to evaluate or compare.
40-
*
41-
* @return {@code true} if {@link #evaluateTransformed} should be called
42-
*/
43-
protected abstract boolean hasTransformedSide();
44-
45-
/**
46-
* Evaluates the original side against the (reduced) database.
31+
* Evaluates the original side against the reduced database.
4732
*
4833
* @param globalState
4934
* the state whose connection points at the reduced database
@@ -56,8 +41,7 @@ protected AbstractComparisonReproducer(String expectedErrorMessage) {
5641
protected abstract R evaluateOriginal(G globalState) throws SQLException;
5742

5843
/**
59-
* Evaluates the transformed side against the (reduced) database. Only called when {@link #hasTransformedSide()} is
60-
* {@code true}.
44+
* Evaluates the transformed side against the reduced database.
6145
*
6246
* @param globalState
6347
* the state whose connection points at the reduced database
@@ -89,21 +73,9 @@ public final boolean bugStillTriggers(G globalState) {
8973
R transformed;
9074
try {
9175
original = evaluateOriginal(globalState);
92-
if (!hasTransformedSide()) {
93-
// the original bug was a DBMS error on the original query alone, which no longer occurs
94-
return false;
95-
}
9676
transformed = evaluateTransformed(globalState);
97-
} catch (AssertionError unexpectedError) {
98-
// a DBMS error reproduces the bug only if the original failure was the same error;
99-
// other errors are artifacts of the reduction (e.g., a removed CREATE TABLE)
100-
return expectedErrorMessage != null
101-
&& expectedErrorMessage.equals(TestOracleUtils.getUnexpectedErrorMessage(unexpectedError));
102-
} catch (SQLException | RuntimeException e) {
103-
return false;
104-
}
105-
if (expectedErrorMessage != null) {
106-
// the original bug was a DBMS error, which no longer occurs
77+
} catch (AssertionError | SQLException | RuntimeException e) {
78+
// any failure re-running the two sides means this reduced database no longer shows the mismatch
10779
return false;
10880
}
10981
return sidesDiffer(original, transformed, globalState);
@@ -112,28 +84,21 @@ public final boolean bugStillTriggers(G globalState) {
11284
@Override
11385
public final String getBugInformation() {
11486
StringBuilder sb = new StringBuilder();
115-
if (expectedErrorMessage != null) {
116-
sb.append("-- On the database set up by the statements above, the following queries trigger an"
117-
+ " unexpected error with message: ").append(expectedErrorMessage).append(System.lineSeparator());
118-
} else {
119-
sb.append(mismatchHeaderLine()).append(System.lineSeparator());
120-
}
87+
sb.append(mismatchHeaderLine()).append(System.lineSeparator());
12188
appendQueryLines(sb);
12289
return sb.toString();
12390
}
12491

12592
/**
126-
* The header line (without trailing line separator) describing the mismatch, used when the original bug was a
127-
* comparison mismatch rather than an error. For example, "-- On the database set up by the statements above, the
128-
* result sets of the following queries mismatch:".
93+
* The header line (without trailing line separator) describing the mismatch. For example, "-- On the database set
94+
* up by the statements above, the result sets of the following queries mismatch:".
12995
*
13096
* @return the mismatch header line
13197
*/
13298
protected abstract String mismatchHeaderLine();
13399

134100
/**
135-
* Appends the failing queries (or statements) to {@code sb}, one commented line each, so the reduced test case is
136-
* self-contained. Called for both the mismatch and the error case, after the header.
101+
* Appends the failing queries to {@code sb}, one commented line each, so the reduced test case is self-contained.
137102
*
138103
* @param sb
139104
* the builder to append to

src/sqlancer/common/oracle/EETOracle.java

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,13 @@ public class EETOracle<Z extends Select<J, E, T, C>, J extends Join<E, T, C>, E
5555

5656
private final class EETReproducer extends AbstractComparisonReproducer<G, List<String>> {
5757
private final String originalQueryString;
58-
// null if the original bug was a DBMS error on the original query alone
5958
private final String transformedQueryString;
6059

61-
EETReproducer(String originalQueryString, String transformedQueryString, String expectedErrorMessage) {
62-
super(expectedErrorMessage);
60+
EETReproducer(String originalQueryString, String transformedQueryString) {
6361
this.originalQueryString = originalQueryString;
6462
this.transformedQueryString = transformedQueryString;
6563
}
6664

67-
@Override
68-
protected boolean hasTransformedSide() {
69-
return transformedQueryString != null;
70-
}
71-
7265
@Override
7366
protected List<String> evaluateOriginal(G globalState) throws SQLException {
7467
// Re-execute against the current (reduced) database instead of comparing against a cached result set,
@@ -100,11 +93,32 @@ protected String mismatchHeaderLine() {
10093

10194
@Override
10295
protected void appendQueryLines(StringBuilder sb) {
103-
sb.append("-- original: ").append(originalQueryString).append(';').append(System.lineSeparator());
96+
renderQueryLines(sb, originalQueryString, transformedQueryString);
97+
}
98+
}
99+
100+
// Renders the failing queries as commented lines, shared by the mismatch and the unexpected-error reproducers.
101+
// transformedQueryString is null when the error struck the original query before any transformation existed.
102+
private static void renderQueryLines(StringBuilder sb, String originalQueryString, String transformedQueryString) {
103+
sb.append("-- original: ").append(originalQueryString).append(';').append(System.lineSeparator());
104+
if (transformedQueryString != null) {
105+
sb.append("-- transformed: ").append(transformedQueryString).append(';').append(System.lineSeparator());
106+
}
107+
}
108+
109+
// Builds the reproducer for an unexpected DBMS error, which re-runs the query (or both queries) and checks the same
110+
// error still fires. transformedQueryString is null when only the original query ran before the error.
111+
private UnexpectedErrorReproducer<G> errorReproducer(String originalQueryString, String transformedQueryString,
112+
String expectedErrorMessage) {
113+
UnexpectedErrorReproducer.Execution<G> execution = globalState -> {
114+
ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, globalState);
104115
if (transformedQueryString != null) {
105-
sb.append("-- transformed: ").append(transformedQueryString).append(';').append(System.lineSeparator());
116+
ComparatorHelper.getResultSetFirstColumnAsString(transformedQueryString, errors, globalState);
106117
}
107-
}
118+
};
119+
StringBuilder sb = new StringBuilder();
120+
renderQueryLines(sb, originalQueryString, transformedQueryString);
121+
return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString());
108122
}
109123

110124
public EETOracle(G state, EETGenerator<Z, J, E, T, C> gen, ExpectedErrors expectedErrors) {
@@ -139,8 +153,8 @@ public void check() throws SQLException {
139153
originalResultSet = ComparatorHelper.getResultSetFirstColumnAsString(originalQueryString, errors, state);
140154
} catch (AssertionError unexpectedError) {
141155
// an unexpected DBMS error on the original query alone is itself a bug worth reducing;
142-
// transformedQueryString is null because no transformed query is involved
143-
reproducer = new EETReproducer(originalQueryString, null,
156+
// there is no transformed query yet, so only the original is replayed
157+
reproducer = errorReproducer(originalQueryString, null,
144158
TestOracleUtils.getUnexpectedErrorMessage(unexpectedError));
145159
throw unexpectedError;
146160
}
@@ -160,14 +174,14 @@ public void check() throws SQLException {
160174
} catch (AssertionError unexpectedError) {
161175
// the semantics-preserving transformation made the query trigger a DBMS error that the
162176
// original did not, which is a bug worth reducing
163-
reproducer = new EETReproducer(originalQueryString, transformedQueryString,
177+
reproducer = errorReproducer(originalQueryString, transformedQueryString,
164178
TestOracleUtils.getUnexpectedErrorMessage(unexpectedError));
165179
throw unexpectedError;
166180
}
167181

168182
// Set the reproducer before the assertion: assumeResultSetsAreEqual throws when the bug is
169183
// detected, so creating the reproducer afterwards would leave it null and prevent any reduction.
170-
reproducer = new EETReproducer(originalQueryString, transformedQueryString, null);
184+
reproducer = new EETReproducer(originalQueryString, transformedQueryString);
171185

172186
ComparatorHelper.assumeResultSetsAreEqual(originalResultSet, transformedResultSet, originalQueryString,
173187
List.of(transformedQueryString), state);

src/sqlancer/common/oracle/NoRECOracle.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,13 @@ private static class NoRECReproducer<G extends SQLGlobalState<?, ?>>
3838
private final String unoptimizedQueryString;
3939

4040
NoRECReproducer(Function<G, Integer> optimizedQuery, Function<G, Integer> unoptimizedQuery,
41-
String optimizedQueryString, String unoptimizedQueryString, String expectedErrorMessage) {
42-
super(expectedErrorMessage);
41+
String optimizedQueryString, String unoptimizedQueryString) {
4342
this.optimizedQuery = optimizedQuery;
4443
this.unoptimizedQuery = unoptimizedQuery;
4544
this.optimizedQueryString = optimizedQueryString;
4645
this.unoptimizedQueryString = unoptimizedQueryString;
4746
}
4847

49-
@Override
50-
protected boolean hasTransformedSide() {
51-
return true;
52-
}
53-
5448
@Override
5549
protected Integer evaluateOriginal(G globalState) {
5650
return optimizedQuery.apply(globalState);
@@ -77,11 +71,29 @@ protected String mismatchHeaderLine() {
7771

7872
@Override
7973
protected void appendQueryLines(StringBuilder sb) {
80-
sb.append("-- optimized: ").append(optimizedQueryString).append(';').append(System.lineSeparator());
81-
sb.append("-- unoptimized: ").append(unoptimizedQueryString).append(';').append(System.lineSeparator());
74+
renderQueryLines(sb, optimizedQueryString, unoptimizedQueryString);
8275
}
8376
}
8477

78+
// Renders the failing queries as commented lines, shared by the mismatch and the unexpected-error reproducers.
79+
private static void renderQueryLines(StringBuilder sb, String optimizedQueryString, String unoptimizedQueryString) {
80+
sb.append("-- optimized: ").append(optimizedQueryString).append(';').append(System.lineSeparator());
81+
sb.append("-- unoptimized: ").append(unoptimizedQueryString).append(';').append(System.lineSeparator());
82+
}
83+
84+
// Builds the reproducer for an unexpected DBMS error, which re-runs both queries and checks the same error fires.
85+
private static <G extends SQLGlobalState<?, ?>> UnexpectedErrorReproducer<G> errorReproducer(
86+
Function<G, Integer> optimizedQuery, Function<G, Integer> unoptimizedQuery, String optimizedQueryString,
87+
String unoptimizedQueryString, String expectedErrorMessage) {
88+
UnexpectedErrorReproducer.Execution<G> execution = globalState -> {
89+
optimizedQuery.apply(globalState);
90+
unoptimizedQuery.apply(globalState);
91+
};
92+
StringBuilder sb = new StringBuilder();
93+
renderQueryLines(sb, optimizedQueryString, unoptimizedQueryString);
94+
return new UnexpectedErrorReproducer<>(execution, expectedErrorMessage, sb.toString());
95+
}
96+
8597
public NoRECOracle(G state, NoRECGenerator<Z, J, E, T, C> gen, ExpectedErrors expectedErrors) {
8698
if (state == null || gen == null || expectedErrors == null) {
8799
throw new IllegalArgumentException("Null variables used to initialize test oracle.");
@@ -128,8 +140,8 @@ public void check() throws SQLException {
128140
optimizedCount = optimizedQuery.apply(state);
129141
unoptimizedCount = unoptimizedQuery.apply(state);
130142
} catch (AssertionError unexpectedError) {
131-
reproducer = new NoRECReproducer<>(optimizedQuery, unoptimizedQuery, optimizedQueryString,
132-
unoptimizedQueryString, TestOracleUtils.getUnexpectedErrorMessage(unexpectedError));
143+
reproducer = errorReproducer(optimizedQuery, unoptimizedQuery, optimizedQueryString, unoptimizedQueryString,
144+
TestOracleUtils.getUnexpectedErrorMessage(unexpectedError));
133145
throw unexpectedError;
134146
}
135147

@@ -139,7 +151,7 @@ public void check() throws SQLException {
139151

140152
if (unoptimizedCount != optimizedCount) {
141153
reproducer = new NoRECReproducer<>(optimizedQuery, unoptimizedQuery, optimizedQueryString,
142-
unoptimizedQueryString, null);
154+
unoptimizedQueryString);
143155

144156
String queryFormatString = "-- %s;\n-- count: %d";
145157
String firstQueryStringWithCount = String.format(queryFormatString, optimizedQueryString, optimizedCount);

0 commit comments

Comments
 (0)