You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/09/01 03:43:22 UTC
(grails-core) 06/11: Add unit coverage for CodecCustomTypeMarshaller and CodecExtensions
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch test/grails-data-mongodb-specs
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 668953f40ee4fd4e75182ecf5f81d165acc18b35
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Aug 30 19:05:08 2026 -0500
Add unit coverage for CodecCustomTypeMarshaller and CodecExtensions
Covers the two remaining untested classes in the codecs package root:
CodecCustomTypeMarshaller's supports/targetType delegation and its
unsupported write/query/read operations, and CodecExtensions' full
surface - the CodecProvider.get() and getCodecForBsonType() lookups
(including CodecRegistryAware wiring), every registered BsonValue
converter (binary/objectId/timestamp/dateTime/string/regex/boolean/
null/double/int32/int64/array/document/decimal128, including the
recursive BsonArray/BsonDocument conversions), and the nested
MapCodec/ListCodec/IntRangeCodec/LocaleCodec/CurrencyCodec/GStringCodec
implementations.
PropertyDecoder/PropertyEncoder/CodecRegistryAware/CodecRegistryProvider
were confirmed to be pure interfaces with no logic and are intentionally
left untested.
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QAjfckcY4EJsxranv1RyGV
---
.../codecs/CodecCustomTypeMarshallerSpec.groovy | 79 ++++
.../bson/codecs/CodecExtensionsSpec.groovy | 450 +++++++++++++++++++++
2 files changed, 529 insertions(+)
diff --git a/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecCustomTypeMarshallerSpec.groovy b/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecCustomTypeMarshallerSpec.groovy
new file mode 100644
index 0000000000..3115bd1edf
--- /dev/null
+++ b/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecCustomTypeMarshallerSpec.groovy
@@ -0,0 +1,79 @@
+/*
+ * 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.grails.datastore.bson.codecs
+
+import org.bson.Document
+import org.bson.codecs.Codec
+import spock.lang.Specification
+
+import org.grails.datastore.mapping.core.Datastore
+import org.grails.datastore.mapping.model.MappingContext
+import org.grails.datastore.mapping.model.PersistentProperty
+
+class CodecCustomTypeMarshallerSpec extends Specification {
+
+ Codec codec = Mock(Codec)
+ MappingContext mappingContext = Stub(MappingContext)
+ CodecCustomTypeMarshaller marshaller = new CodecCustomTypeMarshaller(codec, mappingContext)
+
+ void "supports the exact mapping context it was constructed with"() {
+ expect:
+ marshaller.supports(mappingContext)
+
+ and:
+ !marshaller.supports(Stub(MappingContext))
+ }
+
+ void "never supports a Datastore directly"() {
+ expect:
+ !marshaller.supports(Stub(Datastore))
+ }
+
+ void "reports the codec's encoder class as its target type"() {
+ given:
+ 1 * codec.encoderClass >> String
+
+ expect:
+ marshaller.targetType == String
+ }
+
+ void "write is unsupported, callers must use the codec directly"() {
+ when:
+ marshaller.write(Stub(PersistentProperty), new Document(), new Document())
+
+ then:
+ thrown(UnsupportedOperationException)
+ }
+
+ void "query is unsupported, callers must use the codec directly"() {
+ when:
+ marshaller.query(Stub(PersistentProperty), null, new Document())
+
+ then:
+ thrown(UnsupportedOperationException)
+ }
+
+ void "read is unsupported, callers must use the codec directly"() {
+ when:
+ marshaller.read(Stub(PersistentProperty), new Document())
+
+ then:
+ thrown(UnsupportedOperationException)
+ }
+}
diff --git a/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecExtensionsSpec.groovy b/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecExtensionsSpec.groovy
new file mode 100644
index 0000000000..1ad3df0340
--- /dev/null
+++ b/grails-data-mongodb/bson/src/test/groovy/org/grails/datastore/bson/codecs/CodecExtensionsSpec.groovy
@@ -0,0 +1,450 @@
+/*
+ * 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.grails.datastore.bson.codecs
+
+import java.util.regex.Pattern
+
+import org.bson.BsonArray
+import org.bson.BsonBinary
+import org.bson.BsonBoolean
+import org.bson.BsonDateTime
+import org.bson.BsonDecimal128
+import org.bson.BsonDocument
+import org.bson.BsonDouble
+import org.bson.BsonInt32
+import org.bson.BsonInt64
+import org.bson.BsonMaxKey
+import org.bson.BsonNull
+import org.bson.BsonObjectId
+import org.bson.BsonReader
+import org.bson.BsonRegularExpression
+import org.bson.BsonString
+import org.bson.BsonTimestamp
+import org.bson.BsonType
+import org.bson.BsonValue
+import org.bson.BsonWriter
+import org.bson.codecs.Codec
+import org.bson.codecs.DecoderContext
+import org.bson.codecs.BsonStringCodec
+import org.bson.codecs.DocumentCodec
+import org.bson.codecs.EncoderContext
+import org.bson.codecs.configuration.CodecRegistry
+import org.bson.types.Decimal128
+import org.bson.types.ObjectId
+import org.codehaus.groovy.runtime.GStringImpl
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class CodecExtensionsSpec extends Specification {
+
+ CodecExtensions provider = new CodecExtensions()
+ CodecRegistry codecRegistry = Stub(CodecRegistry)
+
+ @Unroll
+ void "provides a #expectedType.simpleName for #type.simpleName"() {
+ expect:
+ provider.get(type, codecRegistry).class == expectedType
+
+ where:
+ type | expectedType
+ IntRange | CodecExtensions.IntRangeCodec
+ GStringImpl | CodecExtensions.GStringCodec
+ GString | CodecExtensions.GStringCodec
+ Locale | CodecExtensions.LocaleCodec
+ Currency | CodecExtensions.CurrencyCodec
+ List | CodecExtensions.ListCodec
+ ArrayList | CodecExtensions.ListCodec
+ Map | CodecExtensions.MapCodec
+ LinkedHashMap | CodecExtensions.MapCodec
+ HashMap | CodecExtensions.MapCodec
+ }
+
+ void "provides no codec for an unregistered type"() {
+ expect:
+ provider.get(Thread, codecRegistry) == null
+ }
+
+ void "wires the registry into a CodecRegistryAware codec it hands out"() {
+ when:
+ CodecExtensions.MapCodec codec = (CodecExtensions.MapCodec) provider.get(Map, codecRegistry)
+
+ then:
+ codec.codecRegistry.is(codecRegistry)
+ }
+
+ @Unroll
+ void "getCodecForBsonType provides a #expectedType.simpleName for #bsonType"() {
+ expect:
+ CodecExtensions.getCodecForBsonType(bsonType, codecRegistry).class == expectedType
+
+ where:
+ bsonType | expectedType
+ BsonType.ARRAY | CodecExtensions.ListCodec
+ BsonType.DOCUMENT | DocumentCodec
+ BsonType.BOOLEAN | org.bson.codecs.BooleanCodec
+ BsonType.STRING | org.bson.codecs.StringCodec
+ BsonType.OBJECT_ID | org.bson.codecs.ObjectIdCodec
+ }
+
+ void "getCodecForBsonType wires the registry into a CodecRegistryAware codec it hands out"() {
+ when:
+ CodecExtensions.ListCodec codec = (CodecExtensions.ListCodec) CodecExtensions.getCodecForBsonType(BsonType.ARRAY, codecRegistry)
+
+ then:
+ codec.codecRegistry.is(codecRegistry)
+ }
+
+ void "getCodecForBsonType returns null for a bson type with no registered codec"() {
+ expect:
+ CodecExtensions.getCodecForBsonType(BsonType.JAVASCRIPT, codecRegistry) == null
+ }
+
+ void "getBsonConverters returns every registered converter, flattened"() {
+ when:
+ def converters = CodecExtensions.getBsonConverters()
+
+ then:
+ // BsonString and BsonInt32 each have two converters registered; every other listed
+ // type has one - see the static registration block in CodecExtensions.
+ converters.size() >= 15
+ }
+
+ void "getBsonConverter returns null for a BsonValue type with no registered converter"() {
+ expect:
+ CodecExtensions.getBsonConverter(BsonMaxKey) == null
+ }
+
+ @Unroll
+ void "converts #source.class.simpleName to its plain Java equivalent"() {
+ expect:
+ CodecExtensions.getBsonConverter(source.class).convert(source) == expected
+
+ where:
+ source | expected
+ new BsonBinary([1, 2, 3] as byte[]) | ([1, 2, 3] as byte[])
+ new BsonObjectId(new ObjectId('5f1d8f8f8f8f8f8f8f8f8f8f')) | new ObjectId('5f1d8f8f8f8f8f8f8f8f8f8f')
+ new BsonString('hello') | 'hello'
+ new BsonBoolean(true) | true
+ new BsonDouble(3.14d) | 3.14d
+ new BsonInt32(42) | 42
+ new BsonInt64(42L) | 42L
+ new BsonDecimal128(new Decimal128(new BigDecimal('42.50'))) | new BigDecimal('42.50')
+ }
+
+ void "converts a BsonTimestamp to a Date using seconds-to-millis"() {
+ given:
+ BsonTimestamp timestamp = new BsonTimestamp(100, 0)
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonTimestamp).convert(timestamp) == new Date(100 * 1000L)
+ }
+
+ void "converts a BsonDateTime to a Date directly from its millis value"() {
+ given:
+ BsonDateTime dateTime = new BsonDateTime(12345L)
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonDateTime).convert(dateTime) == new Date(12345L)
+ }
+
+ void "converts a BsonRegularExpression to a compiled Pattern"() {
+ given:
+ BsonRegularExpression regex = new BsonRegularExpression('a.*b')
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonRegularExpression).convert(regex).pattern() == 'a.*b'
+ }
+
+ void "converts a BsonNull to null"() {
+ expect:
+ CodecExtensions.getBsonConverter(BsonNull).convert(BsonNull.VALUE) == null
+ }
+
+ void "the first registered BsonString converter yields a CharSequence"() {
+ expect:
+ CodecExtensions.getBsonConverter(BsonString).convert(new BsonString('hi')) instanceof CharSequence
+ }
+
+ void "converts a flat BsonArray to a List, recursively converting each element"() {
+ given:
+ BsonArray array = new BsonArray([new BsonInt32(1), new BsonString('two'), BsonNull.VALUE])
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonArray).convert(array) == [1, 'two', null]
+ }
+
+ void "converts a BsonArray to an Object array, recursively converting each element"() {
+ given:
+ BsonArray array = new BsonArray([new BsonInt32(1), new BsonString('two')])
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonArray).convert(array) == [1, 'two'] as Object[]
+ }
+
+ void "converts a BsonDocument to a Map, recursively converting each value"() {
+ given:
+ BsonDocument document = new BsonDocument()
+ document.put('a', new BsonInt32(1))
+ document.put('b', new BsonString('two'))
+
+ expect:
+ CodecExtensions.getBsonConverter(BsonDocument).convert(document) == [a: 1, b: 'two']
+ }
+
+ void "MapCodec decodes each entry, converting its bson value to the plain Java equivalent"() {
+ given:
+ CodecExtensions.MapCodec codec = new CodecExtensions.MapCodec()
+ BsonStringCodec stringCodec = new BsonStringCodec()
+ CodecRegistry registry = Stub(CodecRegistry)
+ registry.get(BsonString) >> stringCodec
+ codec.codecRegistry = registry
+ BsonReader reader = Mock(BsonReader) {
+ getCurrentBsonType() >> BsonType.STRING
+ readBsonType() >>> [BsonType.STRING, BsonType.END_OF_DOCUMENT]
+ readName() >> 'foo'
+ readString() >> 'bar'
+ }
+
+ when:
+ def result = codec.decode(reader, DecoderContext.builder().build())
+
+ then:
+ 1 * reader.readStartDocument()
+ 1 * reader.readEndDocument()
+ result == [foo: 'bar']
+ }
+
+ void "MapCodec decodes a null bson value as a null map entry"() {
+ given:
+ CodecExtensions.MapCodec codec = new CodecExtensions.MapCodec()
+ CodecRegistry registry = Mock(CodecRegistry)
+ Codec<BsonValue> nullCodec = Mock(Codec)
+ codec.codecRegistry = registry
+ BsonReader reader = Mock(BsonReader) {
+ getCurrentBsonType() >> BsonType.NULL
+ readBsonType() >>> [BsonType.NULL, BsonType.END_OF_DOCUMENT]
+ readName() >> 'foo'
+ }
+
+ when:
+ def result = codec.decode(reader, DecoderContext.builder().build())
+
+ then:
+ 1 * registry.get(_) >> nullCodec
+ 1 * nullCodec.decode(reader, _) >> null
+ result == [foo: null]
+ }
+
+ void "MapCodec encodes each non-null entry through the registry's codec for its runtime type"() {
+ given:
+ CodecExtensions.MapCodec codec = new CodecExtensions.MapCodec()
+ CodecRegistry registry = Mock(CodecRegistry)
+ Codec<String> stringCodec = Mock(Codec)
+ codec.codecRegistry = registry
+ BsonWriter writer = Mock(BsonWriter)
+
+ when:
+ codec.encode(writer, [foo: 'bar'], EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeStartDocument()
+ 1 * writer.writeName('foo')
+ 1 * registry.get(String) >> stringCodec
+ 1 * stringCodec.encode(writer, 'bar', _)
+ 1 * writer.writeEndDocument()
+ }
+
+ void "MapCodec encodes a null entry value as bson null, without a codec lookup"() {
+ given:
+ CodecExtensions.MapCodec codec = new CodecExtensions.MapCodec()
+ CodecRegistry registry = Mock(CodecRegistry)
+ codec.codecRegistry = registry
+ BsonWriter writer = Mock(BsonWriter)
+
+ when:
+ codec.encode(writer, [foo: null], EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeNull()
+ 0 * registry.get(_)
+ }
+
+ void "MapCodec reports Map as its encoder class"() {
+ expect:
+ new CodecExtensions.MapCodec().encoderClass == Map
+ }
+
+ void "ListCodec decodes each element, converting its bson value to the plain Java equivalent"() {
+ given:
+ CodecExtensions.ListCodec codec = new CodecExtensions.ListCodec()
+ BsonStringCodec stringCodec = new BsonStringCodec()
+ CodecRegistry registry = Stub(CodecRegistry)
+ registry.get(BsonString) >> stringCodec
+ codec.codecRegistry = registry
+ BsonReader reader = Mock(BsonReader) {
+ getCurrentBsonType() >> BsonType.STRING
+ readBsonType() >>> [BsonType.STRING, BsonType.END_OF_DOCUMENT]
+ readString() >> 'bar'
+ }
+
+ when:
+ def result = codec.decode(reader, DecoderContext.builder().build())
+
+ then:
+ 1 * reader.readStartArray()
+ 1 * reader.readEndArray()
+ result == ['bar']
+ }
+
+ void "ListCodec encodes each non-null element through the registry's codec for its runtime type"() {
+ given:
+ CodecExtensions.ListCodec codec = new CodecExtensions.ListCodec()
+ CodecRegistry registry = Mock(CodecRegistry)
+ Codec<String> stringCodec = Mock(Codec)
+ codec.codecRegistry = registry
+ BsonWriter writer = Mock(BsonWriter)
+
+ when:
+ codec.encode(writer, ['bar'], EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeStartArray()
+ 1 * registry.get(String) >> stringCodec
+ 1 * stringCodec.encode(writer, 'bar', _)
+ 1 * writer.writeEndArray()
+ }
+
+ void "ListCodec encodes a null element as bson null, without a codec lookup"() {
+ given:
+ CodecExtensions.ListCodec codec = new CodecExtensions.ListCodec()
+ CodecRegistry registry = Mock(CodecRegistry)
+ codec.codecRegistry = registry
+ BsonWriter writer = Mock(BsonWriter)
+
+ when:
+ codec.encode(writer, [null], EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeNull()
+ 0 * registry.get(_)
+ }
+
+ void "ListCodec reports List as its encoder class"() {
+ expect:
+ new CodecExtensions.ListCodec().encoderClass == List
+ }
+
+ void "IntRangeCodec encodes the range's bounds as a two-element array"() {
+ given:
+ BsonWriter writer = Mock(BsonWriter)
+
+ when:
+ new CodecExtensions.IntRangeCodec().encode(writer, 1..5, EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeStartArray()
+ 1 * writer.writeInt32(1)
+ 1 * writer.writeInt32(5)
+ 1 * writer.writeEndArray()
+ }
+
+ void "IntRangeCodec decodes a two-element array back into a range"() {
+ given:
+ BsonReader reader = Mock(BsonReader) {
+ readInt32() >>> [1, 5]
+ }
+
+ expect:
+ new CodecExtensions.IntRangeCodec().decode(reader, DecoderContext.builder().build()) == (1..5)
+ }
+
+ void "IntRangeCodec reports IntRange as its encoder class"() {
+ expect:
+ new CodecExtensions.IntRangeCodec().encoderClass == IntRange
+ }
+
+ void "LocaleCodec round-trips a Locale through its string form"() {
+ given:
+ BsonWriter writer = Mock(BsonWriter)
+ BsonReader reader = Stub(BsonReader) {
+ readString() >> 'en'
+ }
+
+ expect:
+ new CodecExtensions.LocaleCodec().decode(reader, DecoderContext.builder().build()) == new Locale('en')
+
+ when:
+ new CodecExtensions.LocaleCodec().encode(writer, new Locale('en'), EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeString('en')
+ }
+
+ void "LocaleCodec reports Locale as its encoder class"() {
+ expect:
+ new CodecExtensions.LocaleCodec().encoderClass == Locale
+ }
+
+ void "CurrencyCodec round-trips a Currency through its currency code"() {
+ given:
+ BsonWriter writer = Mock(BsonWriter)
+ BsonReader reader = Stub(BsonReader) {
+ readString() >> 'USD'
+ }
+
+ expect:
+ new CodecExtensions.CurrencyCodec().decode(reader, DecoderContext.builder().build()) == Currency.getInstance('USD')
+
+ when:
+ new CodecExtensions.CurrencyCodec().encode(writer, Currency.getInstance('USD'), EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeString('USD')
+ }
+
+ void "CurrencyCodec reports Currency as its encoder class"() {
+ expect:
+ new CodecExtensions.CurrencyCodec().encoderClass == Currency
+ }
+
+ void "GStringCodec round-trips a GString through its string form"() {
+ given:
+ BsonWriter writer = Mock(BsonWriter)
+ BsonReader reader = Stub(BsonReader) {
+ readString() >> 'hello'
+ }
+
+ when:
+ GString decoded = new CodecExtensions.GStringCodec().decode(reader, DecoderContext.builder().build())
+
+ then:
+ decoded.toString() == 'hello'
+
+ when:
+ new CodecExtensions.GStringCodec().encode(writer, decoded, EncoderContext.builder().build())
+
+ then:
+ 1 * writer.writeString('hello')
+ }
+
+ void "GStringCodec reports GString as its encoder class"() {
+ expect:
+ new CodecExtensions.GStringCodec().encoderClass == GString
+ }
+}