[ 
https://issues.apache.org/jira/browse/AVRO-3375?focusedWorklogId=727777&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-727777
 ]

ASF GitHub Bot logged work on AVRO-3375:
----------------------------------------

                Author: ASF GitHub Bot
            Created on: 15/Feb/22 20:05
            Start Date: 15/Feb/22 20:05
    Worklog Time Spent: 10m 
      Work Description: martin-g commented on a change in pull request #1526:
URL: https://github.com/apache/avro/pull/1526#discussion_r807230074



##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/TracingAvroTypeException.java
##########
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.avro;

Review comment:
       ditto

##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/generic/TracingClassCastException.java
##########
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.avro.generic;

Review comment:
       Why in `o.a.avro.generic` ?
   I'd put all path/tracing related classes in one package, but I admit you 
better understand the problem this PR solves than me!

##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/TracingAvroTypeException.java
##########
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.avro;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.avro.path.PathElement;
+import org.apache.avro.util.SchemaUtil;
+
+public class TracingAvroTypeException extends AvroTypeException implements 
PathTracingException<AvroTypeException> {
+  private final List<PathElement> reversePath;
+
+  public TracingAvroTypeException(AvroTypeException cause) {
+    super(cause.getMessage(), cause);
+    this.reversePath = new ArrayList<>(1); // expected to be short

Review comment:
       IMO the expected size should be a bit bigger. Better to waste 
(temporarily) few bytes memory than to resize the array.
   E.g. it could be `2` or `3`

##########
File path: 
lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
##########
@@ -323,14 +326,139 @@ public void writeDoesNotAllowJavaEnumForGenericEnum() 
throws IOException {
   public void writeFieldWithDefaultWithExplicitNullDefaultInSchema() throws 
Exception {
     Schema schema = schemaWithExplicitNullDefault();
     GenericRecord record = createRecordWithDefaultField(schema);
-    writeObject(schema, record);
+    writeObject(record);
   }
 
   @Test
   public void writeFieldWithDefaultWithoutExplicitNullDefaultInSchema() throws 
Exception {
     Schema schema = schemaWithoutExplicitNullDefault();
     GenericRecord record = createRecordWithDefaultField(schema);
-    writeObject(schema, record);
+    writeObject(record);
+  }
+
+  @Test
+  public void testNestedNPEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f4", null);
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          
.contains("RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField(\"a\").f4"));
+    }
+  }
+
+  @Test
+  public void testNPEForMapKeyErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.put(null, map.get("a")); // value is valid, but key is null
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          .contains("null key in map at 
RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField"));
+    }
+  }
+
+  @Test
+  public void testShortPathNPEErrorClarity() throws Exception {
+    try {
+      writeObject(Schema.create(Schema.Type.STRING), null);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(),
+          expected.getMessage().contains("null value for (non-nullable) 
string"));
+    }
+  }
+
+  @Test
+  public void testNestedCCEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f4", 42); // not a string
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (ClassCastException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          
.contains("RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField(\"a\").f4"));
+    }
+  }
+
+  @Test
+  public void testShortPathCCEErrorClarity() throws Exception {
+    try {
+      writeObject(Schema.create(Schema.Type.STRING), 42);
+      Assert.fail("expected to throw");
+    } catch (ClassCastException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(),
+          expected.getMessage().contains("value 42 (a java.lang.Integer) 
cannot be cast to expected type string"));
+    }
+  }
+
+  @Test
+  public void testNestedATEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f3", 42); // not a n enum

Review comment:
       s/a n/an/ ?

##########
File path: lang/java/avro/src/main/java/org/apache/avro/path/PathPredicate.java
##########
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.avro.path;
+
+public interface PathPredicate extends PathElement {

Review comment:
       What is the purpose of PathPredicate ?
   When I read `Predicate` I expect to see boolean-valued expression.

##########
File path: share/test/schemas/RecordWithRequiredFields.avsc
##########
@@ -0,0 +1,53 @@
+{
+  "type" : "record",
+  "name" : "RecordWithRequiredFields",
+  "namespace" : "org.apache.avro",
+  "fields" : [ {
+    "name" : "f0",

Review comment:
       IMO it would be easier to understand the test cases if `fN` fields were 
named after their types, e.g. `stringField` and `enumField`.
   Since this PR makes it easier to pinpoint the problematic field I see no 
problem in using `stringField` several types in the schema.

##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/generic/TracingClassCastException.java
##########
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.avro.generic;
+
+import org.apache.avro.PathTracingException;
+import org.apache.avro.Schema;
+import org.apache.avro.path.PathElement;
+import org.apache.avro.util.SchemaUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * a {@link NullPointerException} with extra fields used to trace back the path
+ * to a null value through an object graph
+ */
+public class TracingClassCastException extends ClassCastException implements 
PathTracingException<ClassCastException> {
+  private final ClassCastException cause;
+  private final Object datum;
+  private final Schema expected;
+  private final boolean customCoderUsed;
+  private final List<PathElement> reversePath;
+
+  public TracingClassCastException(ClassCastException cause, Object datum, 
Schema expected, boolean customCoderUsed) {
+    this.cause = cause;
+    this.datum = datum;
+    this.expected = expected;
+    this.customCoderUsed = customCoderUsed;
+    this.reversePath = new ArrayList<>(1); // assume short
+  }
+
+  @Override
+  public void tracePath(PathElement step) {
+    reversePath.add(step);
+  }
+
+  @Override
+  public ClassCastException getCause() {
+    return cause;
+  }
+
+  /**
+   * @return a hopefully helpful error message
+   */
+  @Override
+  public ClassCastException summarize(Schema root) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("value ").append(SchemaUtil.describe(datum));
+    sb.append(" cannot be cast to expected type 
").append(SchemaUtil.describe(expected));
+    if (reversePath == null || reversePath.isEmpty()) {
+      // very simple "shallow" NPE, no nesting at all, or custom coders used 
means we
+      // have no data
+      if (customCoderUsed) {
+        sb.append(". no further details available as custom coders were used");

Review comment:
       s/no/No/

##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/PathTracingException.java
##########
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.avro;

Review comment:
       can this class be moved to `o.a.a.path` package too ?

##########
File path: 
lang/java/avro/src/main/java/org/apache/avro/generic/TracingNullPointException.java
##########
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.avro.generic;
+
+import org.apache.avro.PathTracingException;
+import org.apache.avro.Schema;
+import org.apache.avro.path.MapKeyPredicate;
+import org.apache.avro.path.PathElement;
+import org.apache.avro.util.SchemaUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * a {@link NullPointerException} with extra fields used to trace back the path
+ * to a null value through an object graph
+ */
+public class TracingNullPointException extends NullPointerException
+    implements PathTracingException<NullPointerException> {
+  private final NullPointerException cause;
+  private final Schema expected;
+  private final boolean customCoderUsed;
+  private final List<PathElement> reversePath;
+
+  public TracingNullPointException(NullPointerException cause, Schema 
expected, boolean customCoderUsed) {
+    this.cause = cause;
+    this.expected = expected;
+    this.customCoderUsed = customCoderUsed;
+    this.reversePath = new ArrayList<>(1); // assume short
+  }
+
+  @Override
+  public void tracePath(PathElement step) {
+    reversePath.add(step);
+  }
+
+  @Override
+  public NullPointerException getCause() {
+    return cause;
+  }
+
+  /**
+   * @return a hopefully helpful error message
+   */
+  @Override
+  public NullPointerException summarize(Schema root) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("null value for (non-nullable) ");
+    if (reversePath == null || reversePath.isEmpty()) {
+      // very simple "shallow" NPE, no nesting at all, or custom coders used 
means we
+      // have no data
+      if (customCoderUsed) {
+        sb.append("field or map key. no further details available as custom 
coders were used");

Review comment:
       s/no/No/

##########
File path: 
lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
##########
@@ -323,14 +326,139 @@ public void writeDoesNotAllowJavaEnumForGenericEnum() 
throws IOException {
   public void writeFieldWithDefaultWithExplicitNullDefaultInSchema() throws 
Exception {
     Schema schema = schemaWithExplicitNullDefault();
     GenericRecord record = createRecordWithDefaultField(schema);
-    writeObject(schema, record);
+    writeObject(record);
   }
 
   @Test
   public void writeFieldWithDefaultWithoutExplicitNullDefaultInSchema() throws 
Exception {
     Schema schema = schemaWithoutExplicitNullDefault();
     GenericRecord record = createRecordWithDefaultField(schema);
-    writeObject(schema, record);
+    writeObject(record);
+  }
+
+  @Test
+  public void testNestedNPEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f4", null);
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          
.contains("RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField(\"a\").f4"));
+    }
+  }
+
+  @Test
+  public void testNPEForMapKeyErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.put(null, map.get("a")); // value is valid, but key is null
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          .contains("null key in map at 
RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField"));
+    }
+  }
+
+  @Test
+  public void testShortPathNPEErrorClarity() throws Exception {
+    try {
+      writeObject(Schema.create(Schema.Type.STRING), null);
+      Assert.fail("expected to throw");
+    } catch (NullPointerException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(),
+          expected.getMessage().contains("null value for (non-nullable) 
string"));
+    }
+  }
+
+  @Test
+  public void testNestedCCEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f4", 42); // not a string
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (ClassCastException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          
.contains("RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField(\"a\").f4"));
+    }
+  }
+
+  @Test
+  public void testShortPathCCEErrorClarity() throws Exception {
+    try {
+      writeObject(Schema.create(Schema.Type.STRING), 42);
+      Assert.fail("expected to throw");
+    } catch (ClassCastException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(),
+          expected.getMessage().contains("value 42 (a java.lang.Integer) 
cannot be cast to expected type string"));
+    }
+  }
+
+  @Test
+  public void testNestedATEErrorClarity() throws Exception {
+    GenericData.Record topLevelRecord = buildComplexRecord();
+    @SuppressWarnings("unchecked")
+    Map<String, GenericData.Record> map = (Map<String, GenericData.Record>) 
((List<GenericData.Record>) ((GenericData.Record) topLevelRecord
+        .get("unionField")).get("arrayField")).get(0).get("mapField");
+    map.get("a").put("f3", 42); // not a n enum
+    try {
+      writeObject(topLevelRecord);
+      Assert.fail("expected to throw");
+    } catch (AvroTypeException expected) {
+      Assert.assertTrue("unexpected message " + expected.getMessage(), 
expected.getMessage()
+          
.contains("RecordWithRequiredFields.unionField[UnionRecord].arrayField[0].mapField(\"a\").f3"));
+      Assert.assertTrue("unexpected message " + expected.getMessage(),
+          expected.getMessage().contains("42 (a java.lang.Integer)"));

Review comment:
       should the exception message say something about `enum` ?
   line 415 has a comment mentioning enum




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Issue Time Tracking
-------------------

    Worklog Id:     (was: 727777)
    Time Spent: 3h  (was: 2h 50m)

> add union branch, array index and map key "path" information to serialization 
> errors
> ------------------------------------------------------------------------------------
>
>                 Key: AVRO-3375
>                 URL: https://issues.apache.org/jira/browse/AVRO-3375
>             Project: Apache Avro
>          Issue Type: Bug
>          Components: java
>    Affects Versions: 1.11.0
>            Reporter: radai rosenblatt
>            Priority: Major
>              Labels: pull-request-available
>          Time Spent: 3h
>  Remaining Estimate: 0h
>
> https://issues.apache.org/jira/browse/AVRO-2514 improved serialization error 
> messages greatly by providing more information about the "location" of a 
> piece of erroneous data in the object graph handed to a serializer.
>  
> however, a few more things can be improved:
>  * no info printed about which union branches were selected/used
>  * no info printed about array indexes or map keys
>  * (subjective) text could be made more concise.



--
This message was sent by Atlassian Jira
(v8.20.1#820001)

Reply via email to