88package org .elasticsearch .xpack .esql ;
99
1010import org .apache .lucene .util .BytesRef ;
11+ import org .elasticsearch .common .Strings ;
1112import org .elasticsearch .common .time .DateFormatter ;
1213import org .elasticsearch .compute .data .Page ;
1314import org .elasticsearch .logging .Logger ;
3940import static org .elasticsearch .xpack .esql .core .util .NumericUtils .unsignedLongAsNumber ;
4041import static org .elasticsearch .xpack .esql .core .util .SpatialCoordinateTypes .CARTESIAN ;
4142import static org .elasticsearch .xpack .esql .core .util .SpatialCoordinateTypes .GEO ;
43+ import static org .hamcrest .MatcherAssert .assertThat ;
4244import static org .hamcrest .Matchers .instanceOf ;
4345import static org .junit .Assert .assertEquals ;
44- import static org .junit .Assert .assertThat ;
4546import static org .junit .Assert .fail ;
4647
4748public final class CsvAssert {
@@ -197,11 +198,13 @@ public static void assertData(
197198 for (int row = 0 ; row < expectedValues .size (); row ++) {
198199 try {
199200 if (row >= actualValues .size ()) {
200- if (dataFailures .isEmpty ()) {
201- fail ("Expected more data but no more entries found after [" + row + "]" );
202- } else {
203- dataFailure (dataFailures , "Expected more data but no more entries found after [" + row + "]\n " );
204- }
201+ dataFailure (
202+ "Expected more data but no more entries found after [" + row + "]" ,
203+ dataFailures ,
204+ expected ,
205+ actualValues ,
206+ valueTransformer
207+ );
205208 }
206209
207210 if (logger != null ) {
@@ -212,51 +215,28 @@ public static void assertData(
212215 var actualRow = actualValues .get (row );
213216
214217 for (int column = 0 ; column < expectedRow .size (); column ++) {
215- var expectedValue = expectedRow .get (column );
216- var actualValue = actualRow .get (column );
217218 var expectedType = expected .columnTypes ().get (column );
219+ var expectedValue = convertExpectedValue (expectedType , expectedRow .get (column ));
220+ var actualValue = actualRow .get (column );
218221
219- if (expectedValue != null ) {
220- // convert the long from CSV back to its STRING form
221- if (expectedType == Type .DATETIME ) {
222- expectedValue = rebuildExpected (expectedValue , Long .class , x -> UTC_DATE_TIME_FORMATTER .formatMillis ((long ) x ));
223- } else if (expectedType == Type .DATE_NANOS ) {
224- expectedValue = rebuildExpected (
225- expectedValue ,
226- Long .class ,
227- x -> DateFormatter .forPattern ("strict_date_optional_time_nanos" ).formatNanos ((long ) x )
228- );
229- } else if (expectedType == Type .GEO_POINT ) {
230- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> GEO .wkbToWkt ((BytesRef ) x ));
231- } else if (expectedType == Type .CARTESIAN_POINT ) {
232- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> CARTESIAN .wkbToWkt ((BytesRef ) x ));
233- } else if (expectedType == Type .GEO_SHAPE ) {
234- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> GEO .wkbToWkt ((BytesRef ) x ));
235- } else if (expectedType == Type .CARTESIAN_SHAPE ) {
236- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> CARTESIAN .wkbToWkt ((BytesRef ) x ));
237- } else if (expectedType == Type .IP ) {
238- // convert BytesRef-packed IP to String, allowing subsequent comparison with what's expected
239- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> DocValueFormat .IP .format ((BytesRef ) x ));
240- } else if (expectedType == Type .VERSION ) {
241- // convert BytesRef-packed Version to String
242- expectedValue = rebuildExpected (expectedValue , BytesRef .class , x -> new Version ((BytesRef ) x ).toString ());
243- } else if (expectedType == UNSIGNED_LONG ) {
244- expectedValue = rebuildExpected (expectedValue , Long .class , x -> unsignedLongAsNumber ((long ) x ));
245- }
246- }
247222 var transformedExpected = valueTransformer .apply (expectedType , expectedValue );
248223 var transformedActual = valueTransformer .apply (expectedType , actualValue );
249224 if (Objects .equals (transformedExpected , transformedActual ) == false ) {
250225 dataFailures .add (new DataFailure (row , column , transformedExpected , transformedActual ));
251226 }
252227 if (dataFailures .size () > 10 ) {
253- dataFailure (dataFailures );
228+ dataFailure ("" , dataFailures , expected , actualValues , valueTransformer );
254229 }
255230 }
256231
257- var delta = actualRow .size () - expectedRow .size ();
258- if (delta > 0 ) {
259- fail ("Plan has extra columns, returned [" + actualRow .size () + "], expected [" + expectedRow .size () + "]" );
232+ if (actualRow .size () != expectedRow .size ()) {
233+ dataFailure (
234+ "Plan has extra columns, returned [" + actualRow .size () + "], expected [" + expectedRow .size () + "]" ,
235+ dataFailures ,
236+ expected ,
237+ actualValues ,
238+ valueTransformer
239+ );
260240 }
261241 } catch (AssertionError ae ) {
262242 if (logger != null && row + 1 < actualValues .size ()) {
@@ -267,21 +247,95 @@ public static void assertData(
267247 }
268248 }
269249 if (dataFailures .isEmpty () == false ) {
270- dataFailure (dataFailures );
250+ dataFailure ("" , dataFailures , expected , actualValues , valueTransformer );
271251 }
272252 if (expectedValues .size () < actualValues .size ()) {
273- fail (
274- "Elasticsearch still has data after [" + expectedValues .size () + "] entries:\n " + row (actualValues , expectedValues .size ())
253+ dataFailure (
254+ "Elasticsearch still has data after [" + expectedValues .size () + "] entries" ,
255+ dataFailures ,
256+ expected ,
257+ actualValues ,
258+ valueTransformer
275259 );
276260 }
277261 }
278262
279- private static void dataFailure (List <DataFailure > dataFailures ) {
280- dataFailure (dataFailures , "" );
263+ private static void dataFailure (
264+ String description ,
265+ List <DataFailure > dataFailures ,
266+ ExpectedResults expectedValues ,
267+ List <List <Object >> actualValues ,
268+ BiFunction <Type , Object , Object > valueTransformer
269+ ) {
270+ var expected = pipeTable (
271+ "Expected:" ,
272+ expectedValues .columnNames (),
273+ expectedValues .columnTypes (),
274+ expectedValues .values (),
275+ (type , value ) -> valueTransformer .apply (type , convertExpectedValue (type , value ))
276+ );
277+ var actual = pipeTable ("Actual:" , expectedValues .columnNames (), expectedValues .columnTypes (), actualValues , valueTransformer );
278+ fail (description + System .lineSeparator () + describeFailures (dataFailures ) + actual + expected );
279+ }
280+
281+ private static final int MAX_ROWS = 25 ;
282+
283+ private static String pipeTable (
284+ String description ,
285+ List <String > headers ,
286+ List <Type > types ,
287+ List <List <Object >> values ,
288+ BiFunction <Type , Object , Object > valueTransformer
289+ ) {
290+ int rows = Math .min (MAX_ROWS , values .size ());
291+ int [] width = new int [headers .size ()];
292+ String [][] printableValues = new String [rows ][headers .size ()];
293+ for (int c = 0 ; c < headers .size (); c ++) {
294+ width [c ] = header (headers .get (c ), types .get (c )).length ();
295+ }
296+ for (int r = 0 ; r < rows ; r ++) {
297+ for (int c = 0 ; c < headers .size (); c ++) {
298+ printableValues [r ][c ] = String .valueOf (valueTransformer .apply (types .get (c ), values .get (r ).get (c )));
299+ width [c ] = Math .max (width [c ], printableValues [r ][c ].length ());
300+ }
301+ }
302+
303+ var result = new StringBuilder ().append (System .lineSeparator ()).append (description ).append (System .lineSeparator ());
304+ // headers
305+ appendPaddedValue (result , header (headers .get (0 ), types .get (0 )), width [0 ]);
306+ for (int c = 1 ; c < width .length ; c ++) {
307+ result .append (" | " );
308+ appendPaddedValue (result , header (headers .get (c ), types .get (c )), width [c ]);
309+ }
310+ result .append (System .lineSeparator ());
311+ // values
312+ for (int r = 0 ; r < printableValues .length ; r ++) {
313+ appendPaddedValue (result , printableValues [r ][0 ], width [0 ]);
314+ for (int c = 1 ; c < printableValues [r ].length ; c ++) {
315+ result .append (" | " );
316+ appendPaddedValue (result , printableValues [r ][c ], width [c ]);
317+ }
318+ result .append (System .lineSeparator ());
319+ }
320+ if (values .size () > rows ) {
321+ result .append ("..." ).append (System .lineSeparator ());
322+ }
323+ return result .toString ();
324+ }
325+
326+ private static String header (String name , Type type ) {
327+ return name + ':' + Strings .toLowercaseAscii (type .name ());
328+ }
329+
330+ private static void appendPaddedValue (StringBuilder result , String value , int width ) {
331+ result .append (value );
332+ for (int i = 0 ; i < width - (value != null ? value .length () : 4 ); i ++) {
333+ result .append (' ' );
334+ }
281335 }
282336
283- private static void dataFailure (List <DataFailure > dataFailures , String prefixError ) {
284- fail ( prefixError + "Data mismatch:\n " + dataFailures .stream ().map (f -> {
337+ private static String describeFailures (List <DataFailure > dataFailures ) {
338+ return "Data mismatch:" + System . lineSeparator () + dataFailures .stream ().map (f -> {
285339 Description description = new StringDescription ();
286340 ListMatcher expected ;
287341 if (f .expected instanceof List <?> e ) {
@@ -299,7 +353,7 @@ private static void dataFailure(List<DataFailure> dataFailures, String prefixErr
299353 expected .describeMismatch (actualList , description );
300354 String prefix = "row " + f .row + " column " + f .column + ":" ;
301355 return prefix + description .toString ().replace ("\n " , "\n " + prefix );
302- }).collect (Collectors .joining (" \n " )));
356+ }).collect (Collectors .joining (System . lineSeparator ( )));
303357 }
304358
305359 private static Comparator <List <Object >> resultRowComparator (List <Type > types ) {
@@ -331,6 +385,30 @@ private static Comparator<List<Object>> resultRowComparator(List<Type> types) {
331385 };
332386 }
333387
388+ private static Object convertExpectedValue (Type expectedType , Object expectedValue ) {
389+ if (expectedValue == null ) {
390+ return null ;
391+ }
392+
393+ // convert the long from CSV back to its STRING form
394+ return switch (expectedType ) {
395+ case DATETIME -> rebuildExpected (expectedValue , Long .class , x -> UTC_DATE_TIME_FORMATTER .formatMillis ((long ) x ));
396+ case DATE_NANOS -> rebuildExpected (
397+ expectedValue ,
398+ Long .class ,
399+ x -> DateFormatter .forPattern ("strict_date_optional_time_nanos" ).formatNanos ((long ) x )
400+ );
401+ case GEO_POINT , GEO_SHAPE -> rebuildExpected (expectedValue , BytesRef .class , x -> GEO .wkbToWkt ((BytesRef ) x ));
402+ case CARTESIAN_POINT , CARTESIAN_SHAPE -> rebuildExpected (expectedValue , BytesRef .class , x -> CARTESIAN .wkbToWkt ((BytesRef ) x ));
403+ case IP -> // convert BytesRef-packed IP to String, allowing subsequent comparison with what's expected
404+ rebuildExpected (expectedValue , BytesRef .class , x -> DocValueFormat .IP .format ((BytesRef ) x ));
405+ case VERSION -> // convert BytesRef-packed Version to String
406+ rebuildExpected (expectedValue , BytesRef .class , x -> new Version ((BytesRef ) x ).toString ());
407+ case UNSIGNED_LONG -> rebuildExpected (expectedValue , Long .class , x -> unsignedLongAsNumber ((long ) x ));
408+ default -> expectedValue ;
409+ };
410+ }
411+
334412 private static Object rebuildExpected (Object expectedValue , Class <?> clazz , Function <Object , Object > mapper ) {
335413 if (List .class .isAssignableFrom (expectedValue .getClass ())) {
336414 assertThat (((List <?>) expectedValue ).get (0 ), instanceOf (clazz ));
0 commit comments