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/10/09 02:39:27 UTC

(grails-core) 17/18: test: prove the frozen classic oracle is stable, complete and refuses a new fixture

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 9.0.x-h7-improvement-phase3
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 3b42810268f1070e63a1c443e43976dc27769122
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Oct 8 20:50:21 2026 -0500

    test: prove the frozen classic oracle is stable, complete and refuses a new fixture
    
    Serialisation is deterministic and independent of insertion order, the recorded
    files are the canonical rendering of what they hold and cover every scanned group
    and class, a fixture without recorded data (or whose classes changed) fails with
    the way to record it without booting the classic binder, and the VERIFY mode
    reports a recorded group the classic binder no longer produces.
    
    Co-Authored-By: Claude Sonnet 5.5 <no...@anthropic.com>
---
 .../cfg/domainbinding/jpa/ClassicOracleSpec.groovy | 242 +++++++++++++++++++++
 1 file changed, 242 insertions(+)

diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/ClassicOracleSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/ClassicOracleSpec.groovy
new file mode 100644
index 0000000000..d6bd6a95e2
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/jpa/ClassicOracleSpec.groovy
@@ -0,0 +1,242 @@
+/*
+ *  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.orm.hibernate.cfg.domainbinding.jpa
+
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * The frozen oracle itself ({@link ClassicOracle}): that its serialisation is deterministic, that the recorded files are the canonical
+ * rendering of what they hold and cover every scanned fixture, that a fixture without recorded data is refused with a message that says how
+ * to record it, and that the VERIFY mode notices a recorded group the classic binder no longer produces. That a changed recorded fact makes
+ * the comparison fail is proven in {@code GrailsDomainGeneratorDifferentialSpec} and {@code GeneratedDomainClassesDdlDifferentialSpec},
+ * which own the comparisons.
+ */
+class ClassicOracleSpec extends Specification {
+
+    static final List<String> ORACLES = ['generator-differential', 'ddl-differential']
+
+    @Shared
+    List<List<Class<?>>> groups = ScannedDomainClasses.groupByAssociation(ScannedDomainClasses.findEntities())
+
+    void "the serialisation is deterministic and independent of the order a map was built in"() {
+        given:
+        Map<String, Object> first = new LinkedHashMap<String, Object>()
+        first.putAll([b: [z: 1, a: [true, null, 'text']], a: null, c: 'x'])
+        Map<String, Object> second = new LinkedHashMap<String, Object>()
+        second.putAll([c: 'x', a: null, b: [a: [true, null, 'text'], z: 1]])
+
+        expect:
+        ClassicOracle.canonical(first) == ClassicOracle.canonical(first)
+        ClassicOracle.canonical(first) == ClassicOracle.canonical(second)
+        ClassicOracle.canonical(first) == '{"b":{"a":[true,null,"text"],"z":1},"c":"x"}'
+        ClassicOracle.canonical(first, true) == '{"a":null,"b":{"a":[true,null,"text"],"z":1},"c":"x"}'
+    }
+
+    void "a serialised value is read back as it was and serialises to the same text"() {
+        given:
+        Map<String, Object> value = [name: 'tab\tand\nnewline "quoted" é', count: 3, flag: false, list: [[x: 1], [x: 2]]]
+
+        when:
+        Object parsed = ClassicOracle.parse(ClassicOracle.canonical(value))
+
+        then:
+        parsed == value
+        ClassicOracle.canonical(parsed) == ClassicOracle.canonical(value)
+        !ClassicOracle.canonical(value).contains('\t')
+        !ClassicOracle.canonical(value).contains('\n')
+    }
+
+    void "what varies from boot to boot is left out"() {
+        expect:
+        ClassicOracle.canonical([name: 'g.A$__clinit__closure1$_closure2@3ff5aef4']) == '{"name":"g.A$__clinit__closure1$_closure2@0"}'
+        ClassicOracle.stableReason('Could not instantiate org.Foo@1b2c3d4e5 (see log)') == 'Could not instantiate org.Foo@# (see log)'
+        ClassicOracle.stableReason('plain reason') == 'plain reason'
+    }
+
+    void "a section is rendered and parsed back to the same lines"() {
+        given:
+        ClassicOracle.Section section = new ClassicOracle.Section('x.First', [members: ['x.First', 'x.Second']])
+        section.add('table', 'first_table', [columns: [id: [nullable: false, default: null]]], true)
+        section.add('script', '', 'CREATE TABLE "T"(\n  "ID" BIGINT\t);', true)
+
+        when:
+        Map<String, ClassicOracle.Section> parsed = ClassicOracle.parseText(section.render())
+
+        then:
+        parsed.keySet() == ['x.First'].toSet()
+        parsed['x.First'].header == [members: ['x.First', 'x.Second']]
+        parsed['x.First'].parsedByKey('table') == ['first_table': [columns: [id: [nullable: false, default: null]]]]
+        parsed['x.First'].parsed('script') == ['CREATE TABLE "T"(\n  "ID" BIGINT\t);']
+        ClassicOracle.render(parsed) == section.render()
+    }
+
+    @Unroll
+    void "the recorded file #oracle is the canonical rendering of what it holds"() {
+        given:
+        String text = ClassicOracle.getResourceAsStream("/${ClassicOracle.RESOURCE_DIRECTORY}/${oracle}.txt").getText('UTF-8')
+
+        expect:
+        ClassicOracle.render(ClassicOracle.read(oracle)) == text
+
+        where:
+        oracle << ORACLES
+    }
+
+    @Unroll
+    void "the recorded file #oracle holds every scanned group, and only those"() {
+        given:
+        ClassicOracle frozen = new ClassicOracle(oracle, ClassicOracle.Mode.FROZEN)
+        Map<String, List<String>> scanned = groups.collectEntries { List<Class<?>> group -> [(group.first().name): group*.name] }
+
+        expect: "the groups, with the classes they are made of, are the ones scanned now"
+        frozen.recordedKeys() == scanned.keySet()
+        scanned.every { String key, List<String> members -> frozen.recordedSection(key).header.members == members }
+
+        and: "every scanned domain class is in one recorded group"
+        scanned.values().flatten().toSet() == ScannedDomainClasses.findEntities()*.name.toSet()
+
+        and: "a group the classic binder cannot boot has its reason and nothing else, and a group it booted has content, but for the few whose entities live in another data source"
+        scanned.keySet().every { String key ->
+            ClassicOracle.Section section = frozen.recordedSection(key)
+            section.header.unbootable == null || (!section.header.unbootable.toString().isEmpty() && section.records.isEmpty())
+        }
+        scanned.keySet().count { String key ->
+            ClassicOracle.Section section = frozen.recordedSection(key)
+            section.header.unbootable == null && section.records.isEmpty()
+        } <= 20
+        scanned.keySet().count { String key -> !frozen.recordedSection(key).records.isEmpty() } > 700
+
+        where:
+        oracle << ORACLES
+    }
+
+    void "the groups the classic binder booted have their entities in the generator oracle and their tables and statements in the DDL oracle"() {
+        given:
+        ClassicOracle generator = new ClassicOracle('generator-differential', ClassicOracle.Mode.FROZEN)
+        ClassicOracle ddl = new ClassicOracle('ddl-differential', ClassicOracle.Mode.FROZEN)
+        Set<String> withEntities = generator.recordedKeys().findAll { String key -> !generator.recordedSection(key).parsedByKey('entity').isEmpty() }
+        Set<String> withTables = ddl.recordedKeys().findAll { String key -> !ddl.recordedSection(key).parsedByKey('table').isEmpty() }
+
+        expect: "the groups the DDL oracle holds tables of are among those the generator oracle holds entities of (the DDL snapshot cannot describe a few more)"
+        withEntities.size() > 700
+        withEntities.containsAll(withTables)
+        withTables.count { String key -> !ddl.recordedSection(key).parsed('script').isEmpty() } > 700
+    }
+
+    @Unroll
+    void "a fixture with no recorded data fails with the way to record it (#oracle)"() {
+        given:
+        ClassicOracle frozen = new ClassicOracle(oracle, ClassicOracle.Mode.FROZEN)
+        boolean booted = false
+
+        when:
+        frozen.section('org.example.NewFixture', ['org.example.NewFixture']) {
+            booted = true
+            return new ClassicOracle.Section('org.example.NewFixture', [members: ['org.example.NewFixture']])
+        }
+
+        then:
+        IllegalStateException e = thrown()
+        e.message.contains('org.example.NewFixture')
+        e.message.contains('-Pgrails.test.refreezeClassicOracle=true')
+        e.message.contains('src/test/resources/classic-oracle/')
+        !booted
+
+        where:
+        oracle << ORACLES
+    }
+
+    @Unroll
+    void "a recorded group whose classes changed fails with the way to record it (#oracle)"() {
+        given:
+        ClassicOracle frozen = new ClassicOracle(oracle, ClassicOracle.Mode.FROZEN)
+        String key = frozen.recordedKeys().first()
+        List<String> members = (List<String>) frozen.recordedSection(key).header.members
+
+        when:
+        frozen.section(key, members + ['org.example.AddedToTheGroup']) { null }
+
+        then:
+        IllegalStateException e = thrown()
+        e.message.contains('-Pgrails.test.refreezeClassicOracle=true')
+
+        where:
+        oracle << ORACLES
+    }
+
+    void "the frozen mode does not call the classic binder for a recorded group and returns the recorded section"() {
+        given:
+        ClassicOracle frozen = new ClassicOracle('ddl-differential', ClassicOracle.Mode.FROZEN)
+        String key = frozen.recordedKeys().first()
+        List<String> members = (List<String>) frozen.recordedSection(key).header.members
+        boolean booted = false
+
+        when:
+        ClassicOracle.Section section = frozen.section(key, members) { booted = true; null }
+
+        then:
+        !booted
+        !frozen.isLive()
+        section.is(frozen.recordedSection(key))
+        frozen.drift.isEmpty()
+    }
+
+    void "the verify mode notices a group the classic binder no longer produces as it was recorded"() {
+        given:
+        ClassicOracle verify = new ClassicOracle('ddl-differential', ClassicOracle.Mode.VERIFY)
+        ClassicOracle.Section recorded = verify.recordedSection(verify.recordedKeys().find {
+            verify.recordedSection(it).header.unbootable == null
+        })
+        List<String> members = (List<String>) recorded.header.members
+        ClassicOracle.Section same = new ClassicOracle.Section(recorded.key, recorded.header)
+        same.records.addAll(recorded.records)
+        ClassicOracle.Section other = new ClassicOracle.Section(recorded.key, recorded.header)
+        other.records.addAll(recorded.records)
+        other.records[0] = [other.records[0][0], other.records[0][1], '{"changed":true}']
+
+        when:
+        verify.section(recorded.key, members) { same }
+
+        then: "the same section is the recorded one"
+        verify.isLive()
+        verify.drift.isEmpty()
+
+        when:
+        verify.section(recorded.key, members) { other }
+
+        then: "a section that differs is reported with the first line that does"
+        verify.drift.size() == 1
+        verify.drift[0].contains(recorded.key)
+        verify.drift[0].contains('differs')
+    }
+
+    void "the verify mode reports recorded groups that were not visited"() {
+        given:
+        ClassicOracle verify = new ClassicOracle('generator-differential', ClassicOracle.Mode.VERIFY)
+
+        when:
+        verify.finish()
+
+        then:
+        verify.drift.size() == verify.recordedKeys().size()
+        verify.drift.every { String line -> line.contains('no scanned fixture') }
+    }
+}