You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2019/01/23 12:09:48 UTC

[groovy] branch master updated: GROOVY-8967: @Immutable not handling property default values from map constructor

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

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 1ffdc5f  GROOVY-8967: @Immutable not handling property default values from map constructor
1ffdc5f is described below

commit 1ffdc5f9764a0dc45a1daf31cf247df89f643756
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Jan 23 21:42:55 2019 +1000

    GROOVY-8967: @Immutable not handling property default values from map constructor
---
 .../options/ImmutablePropertyHandler.java          |  125 +-
 .../groovy/transform/ImmutableTransformTest.groovy | 2183 ++++++++++----------
 2 files changed, 1226 insertions(+), 1082 deletions(-)

diff --git a/src/main/groovy/groovy/transform/options/ImmutablePropertyHandler.java b/src/main/groovy/groovy/transform/options/ImmutablePropertyHandler.java
index f815942..3062fcd 100644
--- a/src/main/groovy/groovy/transform/options/ImmutablePropertyHandler.java
+++ b/src/main/groovy/groovy/transform/options/ImmutablePropertyHandler.java
@@ -26,9 +26,11 @@ import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.ConstantExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.MapExpression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
 import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.EmptyStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
@@ -129,7 +131,7 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         if (fNode.isFinal() && fNode.getInitialExpression() != null) {
             return checkFinalArgNotOverridden(cNode, fNode);
         }
-        return createConstructorStatement(xform, cNode, pNode, namedArgsMap != null);
+        return createConstructorStatement(xform, cNode, pNode, namedArgsMap);
     }
 
     private static Statement createGetterBodyDefault(FieldNode fNode) {
@@ -170,6 +172,7 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return callX(DGM_TYPE, "asImmutable", castX(type, expr));
     }
 
+    @Deprecated
     protected Statement createConstructorStatement(AbstractASTTransformation xform, ClassNode cNode, PropertyNode pNode, boolean namedArgs) {
         final List<String> knownImmutableClasses = ImmutablePropertyUtils.getKnownImmutableClasses(xform, cNode);
         final List<String> knownImmutables = ImmutablePropertyUtils.getKnownImmutables(xform, cNode);
@@ -193,6 +196,29 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return statement;
     }
 
+    protected Statement createConstructorStatement(AbstractASTTransformation xform, ClassNode cNode, PropertyNode pNode, Parameter namedArgsMap) {
+        final List<String> knownImmutableClasses = ImmutablePropertyUtils.getKnownImmutableClasses(xform, cNode);
+        final List<String> knownImmutables = ImmutablePropertyUtils.getKnownImmutables(xform, cNode);
+        FieldNode fNode = pNode.getField();
+        final ClassNode fType = fNode.getType();
+        Statement statement;
+        if (ImmutablePropertyUtils.isKnownImmutableType(fType, knownImmutableClasses) || isKnownImmutable(pNode.getName(), knownImmutables)) {
+            statement = createConstructorStatementDefault(fNode, namedArgsMap);
+        } else if (fType.isArray() || implementsCloneable(fType)) {
+            statement = createConstructorStatementArrayOrCloneable(fNode, namedArgsMap);
+        } else if (derivesFromDate(fType)) {
+            statement = createConstructorStatementDate(fNode, namedArgsMap);
+        } else if (isOrImplements(fType, COLLECTION_TYPE) || fType.isDerivedFrom(COLLECTION_TYPE) || isOrImplements(fType, MAP_TYPE) || fType.isDerivedFrom(MAP_TYPE)) {
+            statement = createConstructorStatementCollection(fNode, namedArgsMap);
+        } else if (fType.isResolved()) {
+            xform.addError(ImmutablePropertyUtils.createErrorMessage(cNode.getName(), fNode.getName(), fType.getName(), "compiling"), fNode);
+            statement = EmptyStatement.INSTANCE;
+        } else {
+            statement = createConstructorStatementGuarded(fNode, namedArgsMap, knownImmutables, knownImmutableClasses);
+        }
+        return statement;
+    }
+
     private static Statement createConstructorStatementDefault(FieldNode fNode, boolean namedArgs) {
         final ClassNode fType = fNode.getType();
         final Expression fieldExpr = propX(varX("this"), fNode.getName());
@@ -213,6 +239,38 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return assignWithDefault(namedArgs, assignInit, param, assignStmt);
     }
 
+    private static Statement createConstructorStatementDefault(FieldNode fNode, Parameter namedArgsMap) {
+        final ClassNode fType = fNode.getType();
+        final Expression fieldExpr = propX(varX("this"), fNode.getName());
+        Expression param = getParam(fNode, namedArgsMap != null);
+        Statement assignStmt = assignS(fieldExpr, castX(fType, param));
+        Expression initExpr = fNode.getInitialValueExpression();
+        Statement assignInit;
+        if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) {
+            if (ClassHelper.isPrimitiveType(fType)) {
+                assignInit = EmptyStatement.INSTANCE;
+            } else {
+                assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION);
+            }
+        } else {
+            assignInit = assignS(fieldExpr, initExpr);
+        }
+        return assignFieldWithDefault(namedArgsMap, fNode, assignStmt, assignInit);
+    }
+
+    private static Statement assignFieldWithDefault(Parameter map, FieldNode fNode, Statement assignStmt, Statement assignInit) {
+        if (map == null) {
+            return assignStmt;
+        }
+        ArgumentListExpression nameArg = args(constX(fNode.getName()));
+        MethodCallExpression var = callX(varX(map), "get", nameArg);
+        var.setImplicitThis(false);
+        MethodCallExpression containsKey = callX(varX(map), "containsKey", nameArg);
+        containsKey.setImplicitThis(false);
+        fNode.getDeclaringClass().getField(fNode.getName()).setInitialValueExpression(null); // to avoid default initialization
+        return ifElseS(containsKey, assignStmt, assignInit);
+    }
+
     private static Statement assignWithDefault(boolean namedArgs, Statement assignInit, Expression param, Statement assignStmt) {
         if (!namedArgs) {
             return assignStmt;
@@ -234,6 +292,21 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return assignWithDefault(namedArgs, assignInit, param, assignStmt);
     }
 
+    private static Statement createConstructorStatementGuarded(FieldNode fNode, Parameter namedArgsMap, List<String> knownImmutables, List<String> knownImmutableClasses) {
+        final Expression fieldExpr = propX(varX("this"), fNode.getName());
+        Expression param = getParam(fNode, namedArgsMap != null);
+        Statement assignStmt = assignS(fieldExpr, checkUnresolved(fNode, param, knownImmutables, knownImmutableClasses));
+        assignStmt = ifElseS(equalsNullX(param), assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION), assignStmt);
+        Expression initExpr = fNode.getInitialValueExpression();
+        final Statement assignInit;
+        if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) {
+            assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION);
+        } else {
+            assignInit = assignS(fieldExpr, checkUnresolved(fNode, initExpr, knownImmutables, knownImmutableClasses));
+        }
+        return assignFieldWithDefault(namedArgsMap, fNode, assignStmt, assignInit);
+    }
+
     private static Expression checkUnresolved(FieldNode fNode, Expression value, List<String> knownImmutables, List<String> knownImmutableClasses) {
         Expression args = args(callThisX("getClass"), constX(fNode.getName()), value, list2args(knownImmutables), classList2args(knownImmutableClasses));
         return callX(SELF_TYPE, "checkImmutable", args);
@@ -257,6 +330,25 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return assignWithDefault(namedArgs, assignInit, param, assignStmt);
     }
 
+    private Statement createConstructorStatementCollection(FieldNode fNode, Parameter namedArgsMap) {
+        final Expression fieldExpr = propX(varX("this"), fNode.getName());
+        ClassNode fieldType = fieldExpr.getType();
+        Expression param = getParam(fNode, namedArgsMap != null);
+        Statement assignStmt = ifElseS(
+                isInstanceOfX(param, CLONEABLE_TYPE),
+                assignS(fieldExpr, cloneCollectionExpr(cloneArrayOrCloneableExpr(param, fieldType), fieldType)),
+                assignS(fieldExpr, cloneCollectionExpr(param, fieldType)));
+        assignStmt = ifElseS(equalsNullX(param), assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION), assignStmt);
+        Expression initExpr = fNode.getInitialValueExpression();
+        final Statement assignInit;
+        if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) {
+            assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION);
+        } else {
+            assignInit = assignS(fieldExpr, cloneCollectionExpr(initExpr, fieldType));
+        }
+        return assignFieldWithDefault(namedArgsMap, fNode, assignStmt, assignInit);
+    }
+
     private static Statement createConstructorStatementArrayOrCloneable(FieldNode fNode, boolean namedArgs) {
         final Expression fieldExpr = propX(varX("this"), fNode.getName());
         final Expression initExpr = fNode.getInitialValueExpression();
@@ -272,6 +364,22 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return assignWithDefault(namedArgs, assignInit, param, assignStmt);
     }
 
+    private static Statement createConstructorStatementArrayOrCloneable(FieldNode fNode, Parameter namedArgsMap) {
+        final Expression fieldExpr = propX(varX("this"), fNode.getName());
+        final ClassNode fieldType = fNode.getType();
+        final Expression param = getParam(fNode, namedArgsMap != null);
+        Statement assignStmt = assignS(fieldExpr, cloneArrayOrCloneableExpr(param, fieldType));
+        assignStmt = ifElseS(equalsNullX(param), assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION), assignStmt);
+        final Statement assignInit;
+        final Expression initExpr = fNode.getInitialValueExpression();
+        if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) {
+            assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION);
+        } else {
+            assignInit = assignS(fieldExpr, cloneArrayOrCloneableExpr(initExpr, fieldType));
+        }
+        return assignFieldWithDefault(namedArgsMap, fNode, assignStmt, assignInit);
+    }
+
     private static Expression getParam(FieldNode fNode, boolean namedArgs) {
         return namedArgs ? findArg(fNode.getName()) : varX(fNode.getName(), fNode.getType());
     }
@@ -290,6 +398,21 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         return assignWithDefault(namedArgs, assignInit, param, assignStmt);
     }
 
+    private static Statement createConstructorStatementDate(FieldNode fNode, Parameter namedArgsMap) {
+        final Expression fieldExpr = propX(varX("this"), fNode.getName());
+        final Expression param = getParam(fNode, namedArgsMap != null);
+        Statement assignStmt = assignS(fieldExpr, cloneDateExpr(param));
+        assignStmt = ifElseS(equalsNullX(param), assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION), assignStmt);
+        final Statement assignInit;
+        Expression initExpr = fNode.getInitialValueExpression();
+        if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) {
+            assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION);
+        } else {
+            assignInit = assignS(fieldExpr, cloneDateExpr(initExpr));
+        }
+        return assignFieldWithDefault(namedArgsMap, fNode, assignStmt, assignInit);
+    }
+
     private static boolean isKnownImmutable(String fieldName, List<String> knownImmutables) {
         return knownImmutables.contains(fieldName);
     }
diff --git a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
index e8a6ddf..5723c47 100644
--- a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
@@ -1,1081 +1,1102 @@
-/*
- *  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
- *
- *    http://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.codehaus.groovy.transform
-
-import org.codehaus.groovy.control.MultipleCompilationErrorsException
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TestName
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-import static org.junit.Assume.assumeTrue
-
-/**
- * Tests for the @Immutable transform.
- */
-@RunWith(JUnit4)
-class ImmutableTransformTest extends GroovyShellTestCase {
-
-    @Rule public TestName nameRule = new TestName()
-
-    @Before
-    void setUp() {
-        super.setUp()
-        // check java version requirements
-        def v = System.getProperty("java.specification.version")
-        assert v
-        assumeTrue('Test requires jre8+', nameRule.methodName.endsWith('_vm8').implies(new BigDecimal(v) >= 1.8))
-    }
-
-    @After
-    void tearDown() {
-        super.tearDown()
-    }
-
-    @Test
-    void testImmutable() {
-        def objects = evaluate('''
-            import groovy.transform.Immutable
-            enum Coin { HEAD, TAIL }
-            @Immutable class Bar {
-                String x, y
-                Coin c
-                Collection nums
-            }
-            [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]),
-             new Bar('x', 'y', Coin.HEAD, [1,2])]
-        ''')
-
-        assert objects[0].hashCode() == objects[1].hashCode()
-        assert objects[0] == objects[1]
-        assert objects[0].nums.class.name.contains("Unmodifiable")
-    }
-
-    @Test
-    void testImmutableClonesListAndCollectionFields() {
-        def objects = evaluate("""
-            import groovy.transform.Immutable
-            def myNums = [1, 2]
-            @Immutable class Bar {
-                List nums
-                Collection otherNums
-            }
-            def myBar = new Bar(nums:myNums, otherNums:myNums)
-            myNums << 3
-            [myNums, myBar]
-        """)
-
-        assertNotSame(objects[0], objects[1].nums)
-        assertNotSame(objects[0], objects[1].otherNums)
-        assertNotSame(objects[1].nums, objects[1].otherNums)
-        assertEquals 3, objects[0].size()
-        assertEquals 2, objects[1].nums.size()
-        assertEquals 2, objects[1].otherNums.size()
-        assertTrue objects[1].nums.class.name.contains("Unmodifiable")
-        assertTrue objects[1].otherNums.class.name.contains("Unmodifiable")
-    }
-
-    @Test
-    void testImmutableField() {
-        def person = evaluate("""
-            import groovy.transform.Immutable
-            @Immutable class Person {
-                boolean married
-            }
-            new Person(married:false)
-        """)
-        shouldFail(ReadOnlyPropertyException) {
-            person.married = true
-        }
-    }
-
-    @Test
-    void testCloneableField() {
-        def (originalDolly, lab) = evaluate("""
-            import groovy.transform.Immutable
-
-            class Dolly implements Cloneable {
-                String name
-            }
-
-            @Immutable class Lab {
-                String name
-                Cloneable clone
-            }
-
-            def dolly = new Dolly(name: "The Sheep")
-            [dolly, new Lab(name: "Area 51", clone: dolly)]
-        """)
-
-        def clonedDolly = lab.clone
-        def clonedDolly2 = lab.clone
-
-        assert lab.name == 'Area 51'
-        assert !originalDolly.is(clonedDolly)
-        assert originalDolly.name == clonedDolly.name
-        assert !clonedDolly2.is(clonedDolly)
-        assert clonedDolly2.name == clonedDolly.name
-    }
-
-    @Test
-    void testCloneableFieldNotCloneableObject() {
-        def cls = shouldFail(CloneNotSupportedException) {
-            def objects = evaluate("""
-                import groovy.transform.Immutable
-
-                class Dolly {
-                    String name
-                }
-
-                @Immutable class Lab {
-                    String name
-                    Cloneable clone
-                }
-
-                def dolly = new Dolly(name: "The Sheep")
-                [dolly, new Lab(name: "Area 51", clone: dolly)]
-            """)
-        }
-
-        assert cls == 'Dolly'
-    }
-
-    @Test
-    void testImmutableListProp() {
-        def objects = evaluate("""
-            import groovy.transform.Immutable
-            @Immutable class HasList {
-                String[] letters
-                List nums
-            }
-            def letters = 'A,B,C'.split(',')
-            def nums = [1, 2]
-            [new HasList(letters:letters, nums:nums),
-             new HasList(letters, nums)]
-        """)
-
-        assertEquals objects[0].hashCode(), objects[1].hashCode()
-        assertEquals objects[0], objects[1]
-        assert objects[0].letters.size() == 3
-        assert objects[0].nums.size() == 2
-    }
-
-    @Test
-    void testImmutableAsMapKey() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable final class HasString {
-                String s
-            }
-            def k1 = new HasString('xyz')
-            def k2 = new HasString('xyz')
-            def map = [(k1):42]
-            assert map[k2] == 42
-        """
-    }
-
-    @Test
-    void testImmutableWithOnlyMap() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable final class HasMap {
-                Map map
-            }
-            new HasMap([:])
-        """
-    }
-
-    @Test
-    void testImmutableWithPrivateStaticFinalField() {
-        assertScript """
-          @groovy.transform.Immutable class Foo {
-              private static final String BAR = 'baz'
-          }
-          assert new Foo().BAR == 'baz'
-      """
-    }
-
-    @Test
-    void testImmutableWithInvalidPropertyName() {
-        def msg = shouldFail(MissingPropertyException) {
-            assertScript """
-                import groovy.transform.Immutable
-                @Immutable class Simple { }
-                new Simple(missing:'Name')
-            """
-        }
-        assert msg.contains('No such property: missing for class: Simple')
-    }
-
-    @Test
-    void testImmutableWithHashMap() {
-        assertScript """
-            import groovy.transform.Immutable
-            import groovy.transform.options.LegacyHashMapPropertyHandler
-            @Immutable(propertyHandler = LegacyHashMapPropertyHandler, noArg = false)
-            final class HasHashMap {
-                HashMap map = [d:4]
-            }
-            assert new HasHashMap([a:1]).map == [a:1]
-            assert new HasHashMap(c:3).map == [c:3]
-            assert new HasHashMap(map:[b:2]).map == [b:2]
-            assert new HasHashMap(null).map == [d:4]
-            assert new HasHashMap().map == [d:4]
-            assert new HasHashMap([:]).map == [:]
-            assert new HasHashMap(map:5, c:3).map == [map:5, c:3]
-            assert new HasHashMap(map:null).map == null
-            assert new HasHashMap(map:[:]).map == [:]
-        """
-    }
-
-    @Test
-    void testDefaultValuesAreImmutable_groovy6293() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Y { Collection c = []; int foo = 1 }
-            def y = new Y(foo: 3)
-            assert y.c.class.name.contains('Unmodifiable')
-            assert y.c == []
-            assert y.foo == 3
-        """
-    }
-
-    @Test
-    void testNoArgConstructor_groovy6473() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Y { Collection c = []; int foo = 1 }
-            def y = new Y()
-            assert y.c.class.name.contains('Unmodifiable')
-            assert y.c == []
-            assert y.foo == 1
-        """
-    }
-
-    @Test
-    void testImmutableEquals() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class This { String value }
-            @Immutable class That { String value }
-            class Other { }
-
-            assert new This('foo') == new This("foo")
-            assert new This('f${"o"}o') == new This("foo")
-
-            assert new This('foo') != new This("bar")
-            assert new This('foo') != new That("foo")
-            assert new This('foo') != new Other()
-            assert new Other() != new This("foo")
-        """
-    }
-
-    @Test
-    void testExistingToString() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Foo {
-                String value
-            }
-            @Immutable class Bar {
-                String value
-                String toString() { 'zzz' + _toString() }
-            }
-            @Immutable class Baz {
-                String value
-                String toString() { 'zzz' + _toString() }
-                def _toString() { 'xxx' }
-            }
-            def foo = new Foo('abc')
-            def bar = new Bar('abc')
-            def baz = new Baz('abc')
-            assert bar.toString() == 'zzz' + foo.toString().replaceAll('Foo', 'Bar')
-            assert baz.toString() == 'zzzxxx'
-        """
-    }
-
-    @Test
-    void testExistingEquals() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Foo {
-                String value
-            }
-            @Immutable class Bar {
-                String value
-                // doesn't follow normal conventions - for testing only
-                boolean equals(other) { value == 'abc' || _equals(other) }
-            }
-            @Immutable class Baz {
-                String value
-                // doesn't follow normal conventions - for testing only
-                boolean equals(Baz other) { value == 'abc' || _equals(other) }
-                def _equals(other) { false }
-            }
-            def foo1 = new Foo('abc')
-            def foo2 = new Foo('abc')
-            def foo3 = new Foo('def')
-            assert foo1 == foo2
-            assert foo1 != foo3
-
-            def bar1 = new Bar('abc')
-            def bar2 = new Bar('abc')
-            def bar3 = new Bar('def')
-            def bar4 = new Bar('def')
-            assert bar1 == bar2
-            assert bar1 == bar3
-            assert bar3 != bar1
-
-            def baz1 = new Baz('abc')
-            def baz2 = new Baz('abc')
-            def baz3 = new Baz('def')
-            def baz4 = new Baz('def')
-            assert baz1 == baz2
-            assert baz1 == baz3
-            assert baz3 != baz1
-            assert baz3 != baz4
-        """
-    }
-
-    @Test
-    void testExistingHashCode() {
-        assertScript """
-            import groovy.transform.Immutable
-            @Immutable class Foo {
-                String value
-            }
-            @Immutable class Bar {
-                String value
-                // doesn't follow normal conventions - for testing only
-                int hashCode() { value == 'abc' ? -1 : _hashCode() }
-            }
-            @Immutable class Baz {
-                String value
-                // doesn't follow normal conventions - for testing only
-                int hashCode() { value == 'abc' ? -1 : _hashCode() }
-                def _hashCode() { -100 }
-            }
-            def foo1 = new Foo('abc')
-            def foo2 = new Foo('abc')
-            assert foo1.hashCode() == foo2.hashCode()
-
-            def bar1 = new Bar('abc')
-            def bar2 = new Bar('def')
-            def bar3 = new Bar('def')
-            assert bar1.hashCode() == -1
-            assert bar2.hashCode() == bar3.hashCode()
-
-            def baz1 = new Baz('abc')
-            def baz2 = new Baz('def')
-            assert baz1.hashCode() == -1
-            assert baz2.hashCode() == -100
-        """
-    }
-
-    @Test
-    void testBuiltinImmutables() {
-        assertScript '''
-            import java.awt.Color
-            import groovy.transform.Immutable
-
-            @Immutable class Person {
-                UUID id
-                String name
-                Date dob
-                Color favColor
-                Class helperType
-            }
-
-            def p = new Person(id: UUID.randomUUID(), name: 'Fred', dob: new Date(), favColor: Color.GREEN, helperType: StringBuffer)
-            def propClasses = [p.id, p.name, p.dob, p.favColor, p.helperType]*.class.name
-            assert propClasses == ['java.util.UUID', 'java.lang.String', 'java.util.Date', 'java.awt.Color', 'java.lang.Class']
-        '''
-    }
-
-    @Test
-    void testPrivateFieldAssignedViaConstructor() {
-        assertScript '''
-            import groovy.transform.Immutable
-            @Immutable(includeStatic = true)
-            class Numbers {
-                private int a1 = 1
-                private int b1 = -1
-                private int c1
-                private final int a2 = 2
-                private final int b2 = -2
-                private final int c2
-                private static int a3 = 3
-                private static int b3 = -3
-                private static int c3
-                private static final int a4 = 4
-                private static final int b4 = -4
-                private static final int c4 = 4
-            }
-            def n1 = new Numbers(b1:1, b3:3, c1:1, c2:2, c3:3)
-            assert [1..4, 'a'..'c'].combinations().collect{ num, let -> n1."$let$num" } ==
-                       [1, 2, 3, 4, 1, -2, 3, -4, 1, 2, 3, 4]
-        '''
-    }
-
-    @Test
-    void testPrivateFinalFieldAssignedViaConstructorShouldCauseError() {
-        shouldFail(ReadOnlyPropertyException) {
-            evaluate '''
-                import groovy.transform.Immutable
-                @Immutable class Numbers {
-                    private final int b2 = -2
-                }
-                def n1 = new Numbers(b2:2)
-            '''
-        }
-    }
-
-    @Test
-    void testImmutableWithImmutableFields() {
-        assertScript '''
-            import groovy.transform.Immutable
-            @Immutable class Bar { Integer i }
-            @Immutable class Foo { Bar b }
-            def fb = new Foo(new Bar(3))
-            assert fb.toString() == 'Foo(Bar(3))'
-        '''
-    }
-
-    @Test
-    void testImmutableWithConstant() {
-        assertScript '''
-            import groovy.transform.Immutable
-            @Immutable class MinIntegerHolder {
-                Integer i
-                public static final MIN = 3
-                Integer getMinI() { [i, MIN].max() }
-            }
-            def mih2 = new MinIntegerHolder(2)
-            def mih4 = new MinIntegerHolder(4)
-            assert mih2.minI == 3
-            assert mih4.minI == 4
-        '''
-    }
-
-    @Test
-    void testStaticsAllowed_ThoughUsuallyBadDesign() {
-        // design here is questionable as getDescription() method is not idempotent
-        assertScript '''
-            import groovy.transform.Immutable
-            @Immutable class Person {
-               String first, last
-               static species = 'Human'
-               String getFullname() {
-                 "$first $last"
-               }
-               String getDescription() {
-                 "$fullname is a $species"
-               }
-            }
-
-            def spock = new Person('Leonard', 'Nimoy')
-            assert spock.species == 'Human'
-            assert spock.fullname == 'Leonard Nimoy'
-            assert spock.description == 'Leonard Nimoy is a Human'
-
-            spock.species = 'Romulan'
-            assert spock.species == 'Romulan'
-
-            Person.species = 'Vulcan'
-            assert spock.species == 'Vulcan'
-            assert spock.fullname == 'Leonard Nimoy'
-            assert spock.description == 'Leonard Nimoy is a Vulcan'
-        '''
-    }
-
-    @Test
-    void testImmutableToStringVariants() {
-        assertScript '''
-            import groovy.transform.*
-
-            @Immutable
-            class Person1 { String first, last }
-
-            @Immutable
-            @ToString(includeNames=true)
-            class Person2 { String first, last }
-
-            @Immutable
-            @ToString(excludes="last")
-            class Person3 { String first, last }
-
-            assert new Person1("Hamlet", "D'Arcy").toString() == "Person1(Hamlet, D'Arcy)"
-            assert new Person2(first: "Hamlet", last: "D'Arcy").toString() == "Person2(first:Hamlet, last:D'Arcy)"
-            assert new Person3("Hamlet", "D'Arcy").toString() == "Person3(Hamlet)"
-        '''
-    }
-
-    @Test
-    void testImmutableUsageOnInnerClasses() {
-        assertScript '''
-            import groovy.transform.Immutable
-            class A4997 {
-                @Immutable
-                static class B4997 { String name }
-                @Immutable
-                class C4997 { String name }
-                def test() {
-                    assert new C4997(name: 'foo').toString() == 'A4997$C4997(foo)'
-                }
-            }
-            assert new A4997.B4997(name: 'bar').toString() == 'A4997$B4997(bar)'
-            new A4997().test()
-        '''
-    }
-
-    @Test
-    void testKnownImmutableClassesWithNamedParameters() {
-        assertScript '''
-            import groovy.transform.*
-            @Immutable(knownImmutableClasses = [Address])
-            class Person {
-                String first, last
-                Address address
-            }
-
-            @TupleConstructor @ToString class Address { final String street }
-
-            assert new Person(first: 'John', last: 'Doe', address: new Address('Some Street')).toString() == 'Person(John, Doe, Address(Some Street))'
-        '''
-    }
-
-    @Test
-    void testKnownImmutableClassesWithExplicitConstructor() {
-        assertScript '''
-            @groovy.transform.Immutable(knownImmutableClasses = [Address])
-            class Person {
-                String first, last
-                Address address
-            }
-
-            // ok, not really immutable but deem it such for the purpose of this test
-            @groovy.transform.ToString class Address { String street }
-
-            assert new Person('John', 'Doe', new Address(street: 'Street')).toString() == 'Person(John, Doe, Address(Street))'
-        '''
-    }
-
-    @Test
-    void testKnownImmutableClassesWithCoercedConstruction() {
-        assertScript '''
-            @groovy.transform.Immutable(knownImmutableClasses = [Address])
-            class Person {
-                String first, last
-                Address address
-            }
-
-            // ok, not really immutable but deem it such for the purpose of this test
-            @groovy.transform.Canonical class Address { String street }
-
-            assert new Person(first: 'John', last: 'Doe', address: ['Street']).toString() == 'Person(John, Doe, Address(Street))'
-        '''
-    }
-
-    @Test
-    void testKnownImmutableClassesMissing() {
-        def msg = shouldFail(RuntimeException) {
-            evaluate '''
-                @groovy.transform.ToString class Address { String street }
-
-                @groovy.transform.Immutable
-                class Person {
-                    String first, last
-                    Address address
-                }
-
-                new Person(first: 'John', last: 'Doe', address: new Address(street: 'Street'))
-            '''
-        }
-        assert msg.contains("Unsupported type (Address) found for field 'address' while constructing immutable class Person")
-        assert msg.contains("Immutable classes only support properties with effectively immutable types")
-    }
-
-    // GROOVY-5828
-    @Test
-    void testKnownImmutableCollectionClass() {
-        assertScript '''
-            @groovy.transform.Immutable
-            class ItemsControl { List list }
-            def itemsControl = new ItemsControl(['Fee', 'Fi', 'Fo', 'Fum'])
-            assert itemsControl.list.class.name.contains('Unmodifiable')
-
-            // ok, Items not really immutable but pretend so for the purpose of this test
-            @groovy.transform.Immutable(knownImmutableClasses = [List])
-            class Items { List list }
-            def items = new Items(['Fee', 'Fi', 'Fo', 'Fum'])
-            assert !items.list.class.name.contains('Unmodifiable')
-        '''
-    }
-
-    // GROOVY-5828
-    @Test
-    void testKnownImmutables() {
-        assertScript '''
-            // ok, Items not really immutable but pretend so for the purpose of this test
-            @groovy.transform.Immutable(knownImmutables = ['list1'])
-            class Items {
-                List list1
-                List list2
-            }
-            def items = new Items(['Fee', 'Fi'], ['Fo', 'Fum'])
-            assert !items.list1.class.name.contains('Unmodifiable')
-            assert items.list2.class.name.contains('Unmodifiable')
-        '''
-    }
-
-    // GROOVY-5449
-    @Test
-    void testShouldNotThrowNPE() {
-        def msg = shouldFail(RuntimeException) {
-            evaluate '''
-            @groovy.transform.Immutable
-            class Person {
-                def name
-            }
-            '''
-        }
-        assert msg.contains("Unsupported type (java.lang.Object or def) found for field 'name' while ")
-    }
-
-    // GROOVY-6192
-    @Test
-    void testWithEqualsAndHashCodeASTOverride() {
-        assertScript '''
-            import groovy.transform.*
-
-            @Immutable
-            @EqualsAndHashCode(includes = ['id'])
-            class B {
-                String id
-                String name
-            }
-
-            assert new B('1', 'foo').equals(new B('1', 'foo2'))
-        '''
-    }
-
-    // GROOVY-6354
-    @Test
-    void testCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |class Person {
-            |    String first, last
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( first:'tim' )
-        assert tim.is( tim2 )
-
-        // This should also be the same instance and no changes
-        def tim3 = tim.copyWith( first:'tim', whatever:true )
-        assert tim.is( tim3 )
-
-        // As should this
-        def tim4 = tim.copyWith( whatever:true )
-        assert tim.is( tim4 )
-
-        // And this
-        def tim5 = tim.copyWith()
-        assert tim.is( tim5 )
-
-        // This should be a new instance with a new firstname
-        def alice = tim.copyWith( first:'alice' )
-        assert tim != alice
-        assert alice.first == 'alice'
-        assert !alice.is( tim )
-    }
-
-    @Test
-    void testGenericsCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |class Person {
-            |    List<String> names
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( [ 'Tim', 'Yates' ] )
-        assert tim.names == [ 'Tim', 'Yates' ]
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( names:[ 'Tim', 'Yates' ] )
-        assert tim.is( tim2 )
-
-        // This should be a new instance
-        def alice = tim.copyWith( names:[ 'Alice', 'Yates' ] )
-        assert tim != alice
-        assert alice.names == [ 'Alice', 'Yates' ]
-        assert !alice.is( tim )
-    }
-
-    @Test
-    void testWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
-            |class Foo {
-            |  String first
-            |  String last
-            |  private String cache
-            |  List<String> nicknames
-            |  def full() {
-            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
-            |    cache
-            |  }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
-        assert tim.full() == 'Tim Yates (tim, nick1)'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
-        assert tim.is( tim2 )
-
-        // This should be a new instance
-        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
-        assert tim != alice
-        assert !alice.is( tim )
-        assert alice.full() == 'Alice Yates (ali)'
-    }
-
-    @Test
-    void testStaticWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
-            |@groovy.transform.CompileStatic
-            |class Foo {
-            |  String first
-            |  String last
-            |  private String cache
-            |  List<String> nicknames
-            |  def full() {
-            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
-            |    cache
-            |  }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
-        assert tim.full() == 'Tim Yates (tim, nick1)'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
-        assert tim.is( tim2 )
-
-        // This should be a new instance
-        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
-        assert tim != alice
-        assert !alice.is( tim )
-        assert alice.full() == 'Alice Yates (ali)'
-    }
-
-    @Test
-    void testTypedWithPrivatesCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith=true)
-            |@groovy.transform.TypeChecked
-            |class Foo {
-            |  String first
-            |  String last
-            |  private String cache
-            |  List<String> nicknames
-            |  def full() {
-            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
-            |    cache
-            |  }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
-        assert tim.full() == 'Tim Yates (tim, nick1)'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
-        assert tim.is( tim2 )
-
-        // This should be a new instance
-        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
-        assert tim != alice
-        assert !alice.is( tim )
-        assert alice.full() == 'Alice Yates (ali)'
-    }
-
-    @Test
-    void testStaticCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |@groovy.transform.CompileStatic
-            |class Person {
-            |    String first, last
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( first:'tim' )
-        assert tim.is( tim2 )
-
-        // This should also be the same instance and no changes
-        def tim3 = tim.copyWith( first:'tim', whatever:true )
-        assert tim.is( tim3 )
-
-        // As should this
-        def tim4 = tim.copyWith( whatever:true )
-        assert tim.is( tim4 )
-
-        // And this
-        def tim5 = tim.copyWith()
-        assert tim.is( tim5 )
-
-        // This should be a new instance with a new firstname
-        def alice = tim.copyWith( first:'alice' )
-        assert tim != alice
-        assert alice.first == 'alice'
-        assert !alice.is( tim )
-    }
-
-    @Test
-    void testTypedCopyWith() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |@groovy.transform.TypeChecked
-            |class Person {
-            |    String first, last
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // This should be the same instance and no changes
-        def tim2 = tim.copyWith( first:'tim' )
-        assert tim.is( tim2 )
-
-        // This should also be the same instance and no changes
-        def tim3 = tim.copyWith( first:'tim', whatever:true )
-        assert tim.is( tim3 )
-
-        // As should this
-        def tim4 = tim.copyWith( whatever:true )
-        assert tim.is( tim4 )
-
-        // And this
-        def tim5 = tim.copyWith()
-        assert tim.is( tim5 )
-
-        // This should be a new instance with a new firstname
-        def alice = tim.copyWith( first:'alice' )
-        assert tim != alice
-        assert alice.first == 'alice'
-        assert !alice.is( tim )
-    }
-
-    @Test
-    void testCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |class Person {
-            |    String first, last
-            |    List<Person> copyWith( i ) {
-            |        (1..i).collect { this }
-            |    }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // Check original copyWith remains
-        def result = tim.copyWith( 2 )
-        assert result.size() == 2
-        assert result.first == [ 'tim', 'tim' ]
-    }
-
-    @Test
-    void testStaticCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |@groovy.transform.CompileStatic
-            |class Person {
-            |    String first, last
-            |    List<Person> copyWith( i ) {
-            |        (1..i).collect { this }
-            |    }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // Check original copyWith remains
-        def result = tim.copyWith( 2 )
-        assert result.size() == 2
-        assert result.first == [ 'tim', 'tim' ]
-    }
-
-    @Test
-    void testTypedCopyWithSkipping() {
-        def tester = new GroovyClassLoader().parseClass(
-                '''@groovy.transform.Immutable(copyWith = true)
-            |@groovy.transform.TypeChecked
-            |class Person {
-            |    String first, last
-            |    List<Person> copyWith( i ) {
-            |        (1..i).collect { this }
-            |    }
-            |}
-            |'''.stripMargin() )
-
-        // One instance
-        def tim = tester.newInstance( first:'tim', last:'yates' )
-        assert tim.first == 'tim'
-
-        // Check original copyWith remains
-        def result = tim.copyWith( 2 )
-        assert result.size() == 2
-        assert result.first == [ 'tim', 'tim' ]
-    }
-
-    // GROOVY-7227
-    @Test
-    void testKnownImmutablesWithInvalidPropertyNameResultsInError() {
-        def message = shouldFail {
-            evaluate """
-               import groovy.transform.Immutable
-               @Immutable(knownImmutables=['sirName'])
-               class Person {
-                   String surName
-               }
-               new Person(surName: "Doe")
-           """
-        }
-        assert message.contains("Error during immutable class processing: 'knownImmutables' property 'sirName' does not exist.")
-    }
-
-    // GROOVY-7162
-    @Test
-    void testImmutableWithSuperClass() {
-        assertScript '''
-            import groovy.transform.*
-
-            @EqualsAndHashCode
-            class Person {
-                String name
-            }
-
-            @Immutable
-            @TupleConstructor(includeSuperProperties=true)
-            @EqualsAndHashCode(callSuper=true)
-            @ToString(includeNames=true, includeSuperProperties=true)
-            class Athlete extends Person {
-                String sport
-            }
-
-            def d1 = new Athlete('Michael Jordan', 'BasketBall')
-            def d2 = new Athlete(name: 'Roger Federer', sport: 'Tennis')
-            assert d1 != d2
-            assert d1.toString() == 'Athlete(sport:BasketBall, name:Michael Jordan)'
-            assert d2.toString() == 'Athlete(sport:Tennis, name:Roger Federer)'
-        '''
-    }
-
-    // GROOVY-7600
-    @Test
-    void testImmutableWithOptional_vm8() {
-        assertScript '''
-            @groovy.transform.Immutable class Person {
-                String name
-                Optional<String> address
-            }
-            def p = new Person('Joe', Optional.of('Home'))
-            assert p.toString() == 'Person(Joe, Optional[Home])'
-            assert p.address.get() == 'Home'
-        '''
-        shouldFail(MultipleCompilationErrorsException) {
-            evaluate '''
-            @groovy.transform.Immutable class Person {
-                String name
-                Optional<Date> address
-            }
-            '''
-        }
-    }
-
-    // GROOVY-7599
-    @Test
-    void testImmutableWithJSR310_vm8() {
-        assertScript '''
-            import groovy.transform.Immutable
-            import java.time.*
-
-            @Immutable
-            class Person {
-              String first, last
-              LocalDate born
-            }
-
-            def mmm = new Person('Fred', 'Brooks', LocalDate.of(1931, Month.APRIL, 19))
-            assert mmm.toString() == 'Person(Fred, Brooks, 1931-04-19)'
-        '''
-    }
-
-    // GROOVY-8416
-    @Test
-    void testMapFriendlyNamedArgs() {
-        assertScript '''
-            import groovy.transform.Immutable
-            @Immutable
-            class Point {
-                int x, y
-            }
-            def coordinates = [x: 1, y: 2]
-            assert coordinates instanceof LinkedHashMap
-            def p1 = new Point(coordinates)
-            assert p1.x == 1
-            def p2 = new Point(new HashMap(coordinates))
-            assert p2.x == 1
-            def p3 = new Point(new TreeMap(coordinates))
-            assert p3.x == 1
-        '''
-    }
-}
+/*
+ *  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
+ *
+ *    http://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.codehaus.groovy.transform
+
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+import static org.junit.Assume.assumeTrue
+
+/**
+ * Tests for the @Immutable transform.
+ */
+@RunWith(JUnit4)
+class ImmutableTransformTest extends GroovyShellTestCase {
+
+    @Rule public TestName nameRule = new TestName()
+
+    @Before
+    void setUp() {
+        super.setUp()
+        // check java version requirements
+        def v = System.getProperty("java.specification.version")
+        assert v
+        assumeTrue('Test requires jre8+', nameRule.methodName.endsWith('_vm8').implies(new BigDecimal(v) >= 1.8))
+    }
+
+    @After
+    void tearDown() {
+        super.tearDown()
+    }
+
+    @Test
+    void testImmutable() {
+        def objects = evaluate('''
+            import groovy.transform.Immutable
+            enum Coin { HEAD, TAIL }
+            @Immutable class Bar {
+                String x, y
+                Coin c
+                Collection nums
+            }
+            [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]),
+             new Bar('x', 'y', Coin.HEAD, [1,2])]
+        ''')
+
+        assert objects[0].hashCode() == objects[1].hashCode()
+        assert objects[0] == objects[1]
+        assert objects[0].nums.class.name.contains("Unmodifiable")
+    }
+
+    @Test
+    void testImmutableClonesListAndCollectionFields() {
+        def objects = evaluate("""
+            import groovy.transform.Immutable
+            def myNums = [1, 2]
+            @Immutable class Bar {
+                List nums
+                Collection otherNums
+            }
+            def myBar = new Bar(nums:myNums, otherNums:myNums)
+            myNums << 3
+            [myNums, myBar]
+        """)
+
+        assertNotSame(objects[0], objects[1].nums)
+        assertNotSame(objects[0], objects[1].otherNums)
+        assertNotSame(objects[1].nums, objects[1].otherNums)
+        assertEquals 3, objects[0].size()
+        assertEquals 2, objects[1].nums.size()
+        assertEquals 2, objects[1].otherNums.size()
+        assertTrue objects[1].nums.class.name.contains("Unmodifiable")
+        assertTrue objects[1].otherNums.class.name.contains("Unmodifiable")
+    }
+
+    @Test
+    void testImmutableField() {
+        def person = evaluate("""
+            import groovy.transform.Immutable
+            @Immutable class Person {
+                boolean married
+            }
+            new Person(married:false)
+        """)
+        shouldFail(ReadOnlyPropertyException) {
+            person.married = true
+        }
+    }
+
+    @Test
+    void testCloneableField() {
+        def (originalDolly, lab) = evaluate("""
+            import groovy.transform.Immutable
+
+            class Dolly implements Cloneable {
+                String name
+            }
+
+            @Immutable class Lab {
+                String name
+                Cloneable clone
+            }
+
+            def dolly = new Dolly(name: "The Sheep")
+            [dolly, new Lab(name: "Area 51", clone: dolly)]
+        """)
+
+        def clonedDolly = lab.clone
+        def clonedDolly2 = lab.clone
+
+        assert lab.name == 'Area 51'
+        assert !originalDolly.is(clonedDolly)
+        assert originalDolly.name == clonedDolly.name
+        assert !clonedDolly2.is(clonedDolly)
+        assert clonedDolly2.name == clonedDolly.name
+    }
+
+    @Test
+    void testCloneableFieldNotCloneableObject() {
+        def cls = shouldFail(CloneNotSupportedException) {
+            def objects = evaluate("""
+                import groovy.transform.Immutable
+
+                class Dolly {
+                    String name
+                }
+
+                @Immutable class Lab {
+                    String name
+                    Cloneable clone
+                }
+
+                def dolly = new Dolly(name: "The Sheep")
+                [dolly, new Lab(name: "Area 51", clone: dolly)]
+            """)
+        }
+
+        assert cls == 'Dolly'
+    }
+
+    @Test
+    void testImmutableListProp() {
+        def objects = evaluate("""
+            import groovy.transform.Immutable
+            @Immutable class HasList {
+                String[] letters
+                List nums
+            }
+            def letters = 'A,B,C'.split(',')
+            def nums = [1, 2]
+            [new HasList(letters:letters, nums:nums),
+             new HasList(letters, nums)]
+        """)
+
+        assertEquals objects[0].hashCode(), objects[1].hashCode()
+        assertEquals objects[0], objects[1]
+        assert objects[0].letters.size() == 3
+        assert objects[0].nums.size() == 2
+    }
+
+    @Test
+    void testImmutableAsMapKey() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable final class HasString {
+                String s
+            }
+            def k1 = new HasString('xyz')
+            def k2 = new HasString('xyz')
+            def map = [(k1):42]
+            assert map[k2] == 42
+        """
+    }
+
+    @Test
+    void testImmutableWithOnlyMap() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable final class HasMap {
+                Map map
+            }
+            new HasMap([:])
+        """
+    }
+
+    @Test
+    void testImmutableWithPrivateStaticFinalField() {
+        assertScript """
+          @groovy.transform.Immutable class Foo {
+              private static final String BAR = 'baz'
+          }
+          assert new Foo().BAR == 'baz'
+      """
+    }
+
+    @Test
+    void testImmutableWithInvalidPropertyName() {
+        def msg = shouldFail(MissingPropertyException) {
+            assertScript """
+                import groovy.transform.Immutable
+                @Immutable class Simple { }
+                new Simple(missing:'Name')
+            """
+        }
+        assert msg.contains('No such property: missing for class: Simple')
+    }
+
+    @Test
+    void testImmutableWithHashMap() {
+        assertScript """
+            import groovy.transform.Immutable
+            import groovy.transform.options.LegacyHashMapPropertyHandler
+            @Immutable(propertyHandler = LegacyHashMapPropertyHandler, noArg = false)
+            final class HasHashMap {
+                HashMap map = [d:4]
+            }
+            assert new HasHashMap([a:1]).map == [a:1]
+            assert new HasHashMap(c:3).map == [c:3]
+            assert new HasHashMap(map:[b:2]).map == [b:2]
+            assert new HasHashMap(null).map == [d:4]
+            assert new HasHashMap().map == [d:4]
+            assert new HasHashMap([:]).map == [:]
+            assert new HasHashMap(map:5, c:3).map == [map:5, c:3]
+            assert new HasHashMap(map:null).map == null
+            assert new HasHashMap(map:[:]).map == [:]
+        """
+    }
+
+    @Test
+    void testDefaultValuesAreImmutable_groovy6293() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class Y { Collection c = []; int foo = 1 }
+            def y = new Y(foo: 3)
+            assert y.c.class.name.contains('Unmodifiable')
+            assert y.c == []
+            assert y.foo == 3
+        """
+    }
+
+    @Test
+    void testNoArgConstructor_groovy6473() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class Y { Collection c = []; int foo = 1 }
+            def y = new Y()
+            assert y.c.class.name.contains('Unmodifiable')
+            assert y.c == []
+            assert y.foo == 1
+        """
+    }
+
+    @Test
+    void testImmutableEquals() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class This { String value }
+            @Immutable class That { String value }
+            class Other { }
+
+            assert new This('foo') == new This("foo")
+            assert new This('f${"o"}o') == new This("foo")
+
+            assert new This('foo') != new This("bar")
+            assert new This('foo') != new That("foo")
+            assert new This('foo') != new Other()
+            assert new Other() != new This("foo")
+        """
+    }
+
+    @Test
+    void testExistingToString() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class Foo {
+                String value
+            }
+            @Immutable class Bar {
+                String value
+                String toString() { 'zzz' + _toString() }
+            }
+            @Immutable class Baz {
+                String value
+                String toString() { 'zzz' + _toString() }
+                def _toString() { 'xxx' }
+            }
+            def foo = new Foo('abc')
+            def bar = new Bar('abc')
+            def baz = new Baz('abc')
+            assert bar.toString() == 'zzz' + foo.toString().replaceAll('Foo', 'Bar')
+            assert baz.toString() == 'zzzxxx'
+        """
+    }
+
+    @Test
+    void testExistingEquals() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class Foo {
+                String value
+            }
+            @Immutable class Bar {
+                String value
+                // doesn't follow normal conventions - for testing only
+                boolean equals(other) { value == 'abc' || _equals(other) }
+            }
+            @Immutable class Baz {
+                String value
+                // doesn't follow normal conventions - for testing only
+                boolean equals(Baz other) { value == 'abc' || _equals(other) }
+                def _equals(other) { false }
+            }
+            def foo1 = new Foo('abc')
+            def foo2 = new Foo('abc')
+            def foo3 = new Foo('def')
+            assert foo1 == foo2
+            assert foo1 != foo3
+
+            def bar1 = new Bar('abc')
+            def bar2 = new Bar('abc')
+            def bar3 = new Bar('def')
+            def bar4 = new Bar('def')
+            assert bar1 == bar2
+            assert bar1 == bar3
+            assert bar3 != bar1
+
+            def baz1 = new Baz('abc')
+            def baz2 = new Baz('abc')
+            def baz3 = new Baz('def')
+            def baz4 = new Baz('def')
+            assert baz1 == baz2
+            assert baz1 == baz3
+            assert baz3 != baz1
+            assert baz3 != baz4
+        """
+    }
+
+    @Test
+    void testExistingHashCode() {
+        assertScript """
+            import groovy.transform.Immutable
+            @Immutable class Foo {
+                String value
+            }
+            @Immutable class Bar {
+                String value
+                // doesn't follow normal conventions - for testing only
+                int hashCode() { value == 'abc' ? -1 : _hashCode() }
+            }
+            @Immutable class Baz {
+                String value
+                // doesn't follow normal conventions - for testing only
+                int hashCode() { value == 'abc' ? -1 : _hashCode() }
+                def _hashCode() { -100 }
+            }
+            def foo1 = new Foo('abc')
+            def foo2 = new Foo('abc')
+            assert foo1.hashCode() == foo2.hashCode()
+
+            def bar1 = new Bar('abc')
+            def bar2 = new Bar('def')
+            def bar3 = new Bar('def')
+            assert bar1.hashCode() == -1
+            assert bar2.hashCode() == bar3.hashCode()
+
+            def baz1 = new Baz('abc')
+            def baz2 = new Baz('def')
+            assert baz1.hashCode() == -1
+            assert baz2.hashCode() == -100
+        """
+    }
+
+    @Test
+    void testBuiltinImmutables() {
+        assertScript '''
+            import java.awt.Color
+            import groovy.transform.Immutable
+
+            @Immutable class Person {
+                UUID id
+                String name
+                Date dob
+                Color favColor
+                Class helperType
+            }
+
+            def p = new Person(id: UUID.randomUUID(), name: 'Fred', dob: new Date(), favColor: Color.GREEN, helperType: StringBuffer)
+            def propClasses = [p.id, p.name, p.dob, p.favColor, p.helperType]*.class.name
+            assert propClasses == ['java.util.UUID', 'java.lang.String', 'java.util.Date', 'java.awt.Color', 'java.lang.Class']
+        '''
+    }
+
+    @Test
+    void testPrivateFieldAssignedViaConstructor() {
+        assertScript '''
+            import groovy.transform.Immutable
+            @Immutable(includeStatic = true)
+            class Numbers {
+                private int a1 = 1
+                private int b1 = -1
+                private int c1
+                private final int a2 = 2
+                private final int b2 = -2
+                private final int c2
+                private static int a3 = 3
+                private static int b3 = -3
+                private static int c3
+                private static final int a4 = 4
+                private static final int b4 = -4
+                private static final int c4 = 4
+            }
+            def n1 = new Numbers(b1:1, b3:3, c1:1, c2:2, c3:3)
+            assert [1..4, 'a'..'c'].combinations().collect{ num, let -> n1."$let$num" } ==
+                       [1, 2, 3, 4, 1, -2, 3, -4, 1, 2, 3, 4]
+        '''
+    }
+
+    @Test
+    void testPrivateFinalFieldAssignedViaConstructorShouldCauseError() {
+        shouldFail(ReadOnlyPropertyException) {
+            evaluate '''
+                import groovy.transform.Immutable
+                @Immutable class Numbers {
+                    private final int b2 = -2
+                }
+                def n1 = new Numbers(b2:2)
+            '''
+        }
+    }
+
+    @Test
+    void testImmutableWithImmutableFields() {
+        assertScript '''
+            import groovy.transform.Immutable
+            @Immutable class Bar { Integer i }
+            @Immutable class Foo { Bar b }
+            def fb = new Foo(new Bar(3))
+            assert fb.toString() == 'Foo(Bar(3))'
+        '''
+    }
+
+    @Test
+    void testImmutableWithConstant() {
+        assertScript '''
+            import groovy.transform.Immutable
+            @Immutable class MinIntegerHolder {
+                Integer i
+                public static final MIN = 3
+                Integer getMinI() { [i, MIN].max() }
+            }
+            def mih2 = new MinIntegerHolder(2)
+            def mih4 = new MinIntegerHolder(4)
+            assert mih2.minI == 3
+            assert mih4.minI == 4
+        '''
+    }
+
+    @Test
+    void testStaticsAllowed_ThoughUsuallyBadDesign() {
+        // design here is questionable as getDescription() method is not idempotent
+        assertScript '''
+            import groovy.transform.Immutable
+            @Immutable class Person {
+               String first, last
+               static species = 'Human'
+               String getFullname() {
+                 "$first $last"
+               }
+               String getDescription() {
+                 "$fullname is a $species"
+               }
+            }
+
+            def spock = new Person('Leonard', 'Nimoy')
+            assert spock.species == 'Human'
+            assert spock.fullname == 'Leonard Nimoy'
+            assert spock.description == 'Leonard Nimoy is a Human'
+
+            spock.species = 'Romulan'
+            assert spock.species == 'Romulan'
+
+            Person.species = 'Vulcan'
+            assert spock.species == 'Vulcan'
+            assert spock.fullname == 'Leonard Nimoy'
+            assert spock.description == 'Leonard Nimoy is a Vulcan'
+        '''
+    }
+
+    @Test
+    void testImmutableToStringVariants() {
+        assertScript '''
+            import groovy.transform.*
+
+            @Immutable
+            class Person1 { String first, last }
+
+            @Immutable
+            @ToString(includeNames=true)
+            class Person2 { String first, last }
+
+            @Immutable
+            @ToString(excludes="last")
+            class Person3 { String first, last }
+
+            assert new Person1("Hamlet", "D'Arcy").toString() == "Person1(Hamlet, D'Arcy)"
+            assert new Person2(first: "Hamlet", last: "D'Arcy").toString() == "Person2(first:Hamlet, last:D'Arcy)"
+            assert new Person3("Hamlet", "D'Arcy").toString() == "Person3(Hamlet)"
+        '''
+    }
+
+    @Test
+    void testImmutableUsageOnInnerClasses() {
+        assertScript '''
+            import groovy.transform.Immutable
+            class A4997 {
+                @Immutable
+                static class B4997 { String name }
+                @Immutable
+                class C4997 { String name }
+                def test() {
+                    assert new C4997(name: 'foo').toString() == 'A4997$C4997(foo)'
+                }
+            }
+            assert new A4997.B4997(name: 'bar').toString() == 'A4997$B4997(bar)'
+            new A4997().test()
+        '''
+    }
+
+    @Test
+    void testKnownImmutableClassesWithNamedParameters() {
+        assertScript '''
+            import groovy.transform.*
+            @Immutable(knownImmutableClasses = [Address])
+            class Person {
+                String first, last
+                Address address
+            }
+
+            @TupleConstructor @ToString class Address { final String street }
+
+            assert new Person(first: 'John', last: 'Doe', address: new Address('Some Street')).toString() == 'Person(John, Doe, Address(Some Street))'
+        '''
+    }
+
+    @Test
+    void testKnownImmutableClassesWithExplicitConstructor() {
+        assertScript '''
+            @groovy.transform.Immutable(knownImmutableClasses = [Address])
+            class Person {
+                String first, last
+                Address address
+            }
+
+            // ok, not really immutable but deem it such for the purpose of this test
+            @groovy.transform.ToString class Address { String street }
+
+            assert new Person('John', 'Doe', new Address(street: 'Street')).toString() == 'Person(John, Doe, Address(Street))'
+        '''
+    }
+
+    @Test
+    void testKnownImmutableClassesWithCoercedConstruction() {
+        assertScript '''
+            @groovy.transform.Immutable(knownImmutableClasses = [Address])
+            class Person {
+                String first, last
+                Address address
+            }
+
+            // ok, not really immutable but deem it such for the purpose of this test
+            @groovy.transform.Canonical class Address { String street }
+
+            assert new Person(first: 'John', last: 'Doe', address: ['Street']).toString() == 'Person(John, Doe, Address(Street))'
+        '''
+    }
+
+    @Test
+    void testKnownImmutableClassesMissing() {
+        def msg = shouldFail(RuntimeException) {
+            evaluate '''
+                @groovy.transform.ToString class Address { String street }
+
+                @groovy.transform.Immutable
+                class Person {
+                    String first, last
+                    Address address
+                }
+
+                new Person(first: 'John', last: 'Doe', address: new Address(street: 'Street'))
+            '''
+        }
+        assert msg.contains("Unsupported type (Address) found for field 'address' while constructing immutable class Person")
+        assert msg.contains("Immutable classes only support properties with effectively immutable types")
+    }
+
+    // GROOVY-5828
+    @Test
+    void testKnownImmutableCollectionClass() {
+        assertScript '''
+            @groovy.transform.Immutable
+            class ItemsControl { List list }
+            def itemsControl = new ItemsControl(['Fee', 'Fi', 'Fo', 'Fum'])
+            assert itemsControl.list.class.name.contains('Unmodifiable')
+
+            // ok, Items not really immutable but pretend so for the purpose of this test
+            @groovy.transform.Immutable(knownImmutableClasses = [List])
+            class Items { List list }
+            def items = new Items(['Fee', 'Fi', 'Fo', 'Fum'])
+            assert !items.list.class.name.contains('Unmodifiable')
+        '''
+    }
+
+    // GROOVY-5828
+    @Test
+    void testKnownImmutables() {
+        assertScript '''
+            // ok, Items not really immutable but pretend so for the purpose of this test
+            @groovy.transform.Immutable(knownImmutables = ['list1'])
+            class Items {
+                List list1
+                List list2
+            }
+            def items = new Items(['Fee', 'Fi'], ['Fo', 'Fum'])
+            assert !items.list1.class.name.contains('Unmodifiable')
+            assert items.list2.class.name.contains('Unmodifiable')
+        '''
+    }
+
+    // GROOVY-5449
+    @Test
+    void testShouldNotThrowNPE() {
+        def msg = shouldFail(RuntimeException) {
+            evaluate '''
+            @groovy.transform.Immutable
+            class Person {
+                def name
+            }
+            '''
+        }
+        assert msg.contains("Unsupported type (java.lang.Object or def) found for field 'name' while ")
+    }
+
+    // GROOVY-6192
+    @Test
+    void testWithEqualsAndHashCodeASTOverride() {
+        assertScript '''
+            import groovy.transform.*
+
+            @Immutable
+            @EqualsAndHashCode(includes = ['id'])
+            class B {
+                String id
+                String name
+            }
+
+            assert new B('1', 'foo').equals(new B('1', 'foo2'))
+        '''
+    }
+
+    // GROOVY-6354
+    @Test
+    void testCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |class Person {
+            |    String first, last
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( first:'tim' )
+        assert tim.is( tim2 )
+
+        // This should also be the same instance and no changes
+        def tim3 = tim.copyWith( first:'tim', whatever:true )
+        assert tim.is( tim3 )
+
+        // As should this
+        def tim4 = tim.copyWith( whatever:true )
+        assert tim.is( tim4 )
+
+        // And this
+        def tim5 = tim.copyWith()
+        assert tim.is( tim5 )
+
+        // This should be a new instance with a new firstname
+        def alice = tim.copyWith( first:'alice' )
+        assert tim != alice
+        assert alice.first == 'alice'
+        assert !alice.is( tim )
+    }
+
+    @Test
+    void testGenericsCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |class Person {
+            |    List<String> names
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( [ 'Tim', 'Yates' ] )
+        assert tim.names == [ 'Tim', 'Yates' ]
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( names:[ 'Tim', 'Yates' ] )
+        assert tim.is( tim2 )
+
+        // This should be a new instance
+        def alice = tim.copyWith( names:[ 'Alice', 'Yates' ] )
+        assert tim != alice
+        assert alice.names == [ 'Alice', 'Yates' ]
+        assert !alice.is( tim )
+    }
+
+    @Test
+    void testWithPrivatesCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith=true)
+            |class Foo {
+            |  String first
+            |  String last
+            |  private String cache
+            |  List<String> nicknames
+            |  def full() {
+            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
+            |    cache
+            |  }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
+        assert tim.full() == 'Tim Yates (tim, nick1)'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
+        assert tim.is( tim2 )
+
+        // This should be a new instance
+        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
+        assert tim != alice
+        assert !alice.is( tim )
+        assert alice.full() == 'Alice Yates (ali)'
+    }
+
+    @Test
+    void testStaticWithPrivatesCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith=true)
+            |@groovy.transform.CompileStatic
+            |class Foo {
+            |  String first
+            |  String last
+            |  private String cache
+            |  List<String> nicknames
+            |  def full() {
+            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
+            |    cache
+            |  }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
+        assert tim.full() == 'Tim Yates (tim, nick1)'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
+        assert tim.is( tim2 )
+
+        // This should be a new instance
+        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
+        assert tim != alice
+        assert !alice.is( tim )
+        assert alice.full() == 'Alice Yates (ali)'
+    }
+
+    @Test
+    void testTypedWithPrivatesCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith=true)
+            |@groovy.transform.TypeChecked
+            |class Foo {
+            |  String first
+            |  String last
+            |  private String cache
+            |  List<String> nicknames
+            |  def full() {
+            |    if (!cache) cache = "$first $last (${nicknames.join(', ')})"
+            |    cache
+            |  }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( 'Tim', 'Yates', [ 'tim', 'nick1' ] )
+        assert tim.full() == 'Tim Yates (tim, nick1)'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( nicknames:[ 'tim', 'nick1' ] )
+        assert tim.is( tim2 )
+
+        // This should be a new instance
+        def alice = tim.copyWith( first:'Alice', nicknames:[ 'ali' ] )
+        assert tim != alice
+        assert !alice.is( tim )
+        assert alice.full() == 'Alice Yates (ali)'
+    }
+
+    @Test
+    void testStaticCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |@groovy.transform.CompileStatic
+            |class Person {
+            |    String first, last
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( first:'tim' )
+        assert tim.is( tim2 )
+
+        // This should also be the same instance and no changes
+        def tim3 = tim.copyWith( first:'tim', whatever:true )
+        assert tim.is( tim3 )
+
+        // As should this
+        def tim4 = tim.copyWith( whatever:true )
+        assert tim.is( tim4 )
+
+        // And this
+        def tim5 = tim.copyWith()
+        assert tim.is( tim5 )
+
+        // This should be a new instance with a new firstname
+        def alice = tim.copyWith( first:'alice' )
+        assert tim != alice
+        assert alice.first == 'alice'
+        assert !alice.is( tim )
+    }
+
+    @Test
+    void testTypedCopyWith() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |@groovy.transform.TypeChecked
+            |class Person {
+            |    String first, last
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // This should be the same instance and no changes
+        def tim2 = tim.copyWith( first:'tim' )
+        assert tim.is( tim2 )
+
+        // This should also be the same instance and no changes
+        def tim3 = tim.copyWith( first:'tim', whatever:true )
+        assert tim.is( tim3 )
+
+        // As should this
+        def tim4 = tim.copyWith( whatever:true )
+        assert tim.is( tim4 )
+
+        // And this
+        def tim5 = tim.copyWith()
+        assert tim.is( tim5 )
+
+        // This should be a new instance with a new firstname
+        def alice = tim.copyWith( first:'alice' )
+        assert tim != alice
+        assert alice.first == 'alice'
+        assert !alice.is( tim )
+    }
+
+    @Test
+    void testCopyWithSkipping() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |class Person {
+            |    String first, last
+            |    List<Person> copyWith( i ) {
+            |        (1..i).collect { this }
+            |    }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // Check original copyWith remains
+        def result = tim.copyWith( 2 )
+        assert result.size() == 2
+        assert result.first == [ 'tim', 'tim' ]
+    }
+
+    @Test
+    void testStaticCopyWithSkipping() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |@groovy.transform.CompileStatic
+            |class Person {
+            |    String first, last
+            |    List<Person> copyWith( i ) {
+            |        (1..i).collect { this }
+            |    }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // Check original copyWith remains
+        def result = tim.copyWith( 2 )
+        assert result.size() == 2
+        assert result.first == [ 'tim', 'tim' ]
+    }
+
+    @Test
+    void testTypedCopyWithSkipping() {
+        def tester = new GroovyClassLoader().parseClass(
+                '''@groovy.transform.Immutable(copyWith = true)
+            |@groovy.transform.TypeChecked
+            |class Person {
+            |    String first, last
+            |    List<Person> copyWith( i ) {
+            |        (1..i).collect { this }
+            |    }
+            |}
+            |'''.stripMargin() )
+
+        // One instance
+        def tim = tester.newInstance( first:'tim', last:'yates' )
+        assert tim.first == 'tim'
+
+        // Check original copyWith remains
+        def result = tim.copyWith( 2 )
+        assert result.size() == 2
+        assert result.first == [ 'tim', 'tim' ]
+    }
+
+    // GROOVY-7227
+    @Test
+    void testKnownImmutablesWithInvalidPropertyNameResultsInError() {
+        def message = shouldFail {
+            evaluate """
+               import groovy.transform.Immutable
+               @Immutable(knownImmutables=['sirName'])
+               class Person {
+                   String surName
+               }
+               new Person(surName: "Doe")
+           """
+        }
+        assert message.contains("Error during immutable class processing: 'knownImmutables' property 'sirName' does not exist.")
+    }
+
+    // GROOVY-7162
+    @Test
+    void testImmutableWithSuperClass() {
+        assertScript '''
+            import groovy.transform.*
+
+            @EqualsAndHashCode
+            class Person {
+                String name
+            }
+
+            @Immutable
+            @TupleConstructor(includeSuperProperties=true)
+            @EqualsAndHashCode(callSuper=true)
+            @ToString(includeNames=true, includeSuperProperties=true)
+            class Athlete extends Person {
+                String sport
+            }
+
+            def d1 = new Athlete('Michael Jordan', 'BasketBall')
+            def d2 = new Athlete(name: 'Roger Federer', sport: 'Tennis')
+            assert d1 != d2
+            assert d1.toString() == 'Athlete(sport:BasketBall, name:Michael Jordan)'
+            assert d2.toString() == 'Athlete(sport:Tennis, name:Roger Federer)'
+        '''
+    }
+
+    // GROOVY-7600
+    @Test
+    void testImmutableWithOptional_vm8() {
+        assertScript '''
+            @groovy.transform.Immutable class Person {
+                String name
+                Optional<String> address
+            }
+            def p = new Person('Joe', Optional.of('Home'))
+            assert p.toString() == 'Person(Joe, Optional[Home])'
+            assert p.address.get() == 'Home'
+        '''
+        shouldFail(MultipleCompilationErrorsException) {
+            evaluate '''
+            @groovy.transform.Immutable class Person {
+                String name
+                Optional<Date> address
+            }
+            '''
+        }
+    }
+
+    // GROOVY-7599
+    @Test
+    void testImmutableWithJSR310_vm8() {
+        assertScript '''
+            import groovy.transform.Immutable
+            import java.time.*
+
+            @Immutable
+            class Person {
+              String first, last
+              LocalDate born
+            }
+
+            def mmm = new Person('Fred', 'Brooks', LocalDate.of(1931, Month.APRIL, 19))
+            assert mmm.toString() == 'Person(Fred, Brooks, 1931-04-19)'
+        '''
+    }
+
+    // GROOVY-8416
+    @Test
+    void testMapFriendlyNamedArgs() {
+        assertScript '''
+            import groovy.transform.Immutable
+            @Immutable
+            class Point {
+                int x, y
+            }
+            def coordinates = [x: 1, y: 2]
+            assert coordinates instanceof LinkedHashMap
+            def p1 = new Point(coordinates)
+            assert p1.x == 1
+            def p2 = new Point(new HashMap(coordinates))
+            assert p2.x == 1
+            def p3 = new Point(new TreeMap(coordinates))
+            assert p3.x == 1
+        '''
+    }
+
+    // GROOVY-8967
+    @Test
+    void testPropertiesWithDefaultValues() {
+        assertScript '''
+            import groovy.transform.*
+
+            @Immutable
+            class Thing {
+                int i = 42
+                Date d = new Date()
+                Collection c = [42]
+                String value = "default"
+            }
+
+            def thing = new Thing()
+            assert thing.with{ [i, c, d.class, value] } == [42, [42], Date, 'default']
+            thing = new Thing(c: null, d: null, value: null, i: -1)
+            assert thing.with{ [i, c, d, value] } == [-1, null, null, null]
+        '''
+    }
+}