test/jdk/com/sun/jdi/RedefineNestmateAttr/TestNestmateAttr.java
changeset 50735 2f2af62dfac7
child 50952 529e8aec67bd
equal deleted inserted replaced
50734:0828a0f6676b 50735:2f2af62dfac7
       
     1 /*
       
     2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.
       
     8  *
       
     9  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    12  * version 2 for more details (a copy is included in the LICENSE file that
       
    13  * accompanied this code).
       
    14  *
       
    15  * You should have received a copy of the GNU General Public License version
       
    16  * 2 along with this work; if not, write to the Free Software Foundation,
       
    17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    18  *
       
    19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    20  * or visit www.oracle.com if you need additional information or have any
       
    21  * questions.
       
    22  */
       
    23 
       
    24 /*
       
    25  * @test
       
    26  * @bug 8046171
       
    27  * @summary Class redefinition must preclude changes to nest attributes
       
    28  * @comment This is a copy of test/jdk/java/lang/instrument/RedefineNestmateAttr/
       
    29  * @comment modified for JDI
       
    30  * @library /test/lib ..
       
    31  * @modules java.compiler
       
    32  * @run build TestScaffold VMConnection TargetListener TargetAdapter
       
    33  * @compile NamedBuffer.java
       
    34  * @compile Host/Host.java
       
    35  * @run main/othervm TestNestmateAttr Host
       
    36  * @compile HostA/Host.java
       
    37  * @run main/othervm TestNestmateAttr HostA
       
    38  * @compile HostAB/Host.java
       
    39  * @run main/othervm TestNestmateAttr HostAB
       
    40  * @compile HostABC/Host.java
       
    41  * @run main/othervm TestNestmateAttr HostABC
       
    42  */
       
    43 
       
    44 /* Test Description
       
    45 
       
    46 The basic test class is called Host and we have variants that have zero or more
       
    47 nested classes named A, B, C etc. Each variant of Host is defined in source
       
    48 code in its own directory i.e.
       
    49 
       
    50 Host/Host.java defines zero nested classes
       
    51 HostA/Host.java defines one nested class A
       
    52 HostAB/Host.java defines two nested classes A and B (in that order)
       
    53 etc.
       
    54 
       
    55 Each Host class has the form:
       
    56 
       
    57   public class Host {
       
    58     public static String getID() { return "<directory name>/Host.java"; }
       
    59 
       
    60     < zero or more empty nested classes>
       
    61 
       
    62     public int m() {
       
    63         return 1; // original class
       
    64     }
       
    65   }
       
    66 
       
    67 Under each directory is a directory "redef" with a modified version of the Host
       
    68 class that changes the ID to e.g. Host/redef/Host.java, and the method m()
       
    69 returns 2. This allows us to check we have the redefined class loaded.
       
    70 
       
    71 Using Host' to represent the redefined version we test redefinition
       
    72 combinations as follows:
       
    73 
       
    74 Host:
       
    75   Host -> Host'  - succeeds m() returns 2
       
    76   Host -> HostA' - fails - added a nest member
       
    77 
       
    78 HostA:
       
    79   HostA -> HostA'  - succeeds m() returns 2
       
    80   HostA -> Host'   - fails - removed a nest member
       
    81   HostA -> HostAB' - fails - added a nest member
       
    82   HostA -> HostB'  - fails - replaced a nest member
       
    83 
       
    84 HostAB:
       
    85   HostAB -> HostAB'  - succeeds m() returns 2
       
    86   HostAB -> HostBA'  - succeeds m() returns 2
       
    87   HostAB -> HostA'   - fails - removed a nest member
       
    88   HostAB -> HostABC' - fails - added a nest member
       
    89   HostAB -> HostAC'  - fails - replaced a nest member
       
    90 
       
    91 HostABC:
       
    92   HostABC -> HostABC'  - succeeds m() returns 2
       
    93   HostABC -> HostACB'  - succeeds m() returns 2
       
    94   HostABC -> HostBAC'  - succeeds m() returns 2
       
    95   HostABC -> HostBCA'  - succeeds m() returns 2
       
    96   HostABC -> HostCAB'  - succeeds m() returns 2
       
    97   HostABC -> HostCBA'  - succeeds m() returns 2
       
    98   HostABC -> HostAB'   - fails - removed a nest member
       
    99   HostABC -> HostABCD' - fails - added a nest member
       
   100   HostABC -> HostABD'  - fails - replaced a nest member
       
   101 
       
   102 More than three nested classes doesn't add to the code coverage so
       
   103 we stop here.
       
   104 
       
   105 Note that we always try to load the redefined version even when we expect it
       
   106 to fail.
       
   107 
       
   108 We can only directly load one class Host per classloader, so to run all the
       
   109 groups we either need to use new classloaders, or we reinvoke the test
       
   110 requesting a different primary directory. We chose the latter using
       
   111 multiple @run tags. So we proceed as follows:
       
   112 
       
   113  @compile Host/Host.java
       
   114  @run TestNestmateAttr Host
       
   115  @compile HostA/Host.java  - replaces previous Host.class
       
   116  @run TestNestmateAttr HostA
       
   117  @compile HostAB/Host.java  - replaces previous Host.class
       
   118  @run TestNestmateAttr HostAB
       
   119 etc.
       
   120 
       
   121 Within the test we directly compile redefined versions of the classes,
       
   122 using CompilerUtil, and then read the .class file directly as a byte[].
       
   123 
       
   124 Finally we test redefinition of the NestHost attribute - which is
       
   125 conceptually simple, but in fact very tricky to do. We do that
       
   126 when testing HostA so we can reuse the Host$A class.
       
   127 
       
   128 */
       
   129 
       
   130 import com.sun.jdi.*;
       
   131 import com.sun.jdi.event.*;
       
   132 import com.sun.jdi.request.*;
       
   133 
       
   134 import java.io.File;
       
   135 import java.io.FileInputStream;
       
   136 import java.util.ArrayList;
       
   137 import java.util.Arrays;
       
   138 import java.util.Collections;
       
   139 import java.util.HashMap;
       
   140 import java.util.List;
       
   141 import java.util.Map;
       
   142 
       
   143 import jdk.test.lib.ByteCodeLoader;
       
   144 import jdk.test.lib.compiler.CompilerUtils;
       
   145 import jdk.test.lib.compiler.InMemoryJavaCompiler;
       
   146 import static jdk.test.lib.Asserts.assertTrue;
       
   147 
       
   148 /* For JDI the test is split across two VMs and so split into
       
   149    two main classes. This is the class we will run under the debugger.
       
   150    Package access so we can define in the same source file for ease of
       
   151    reference.
       
   152 */
       
   153 class Target {
       
   154     // We have to load all of the variants of the classes that we will
       
   155     // attempt to redefine. This requires some in-memory compilation
       
   156     // and use of additional classloaders.
       
   157     public static void main(String[] args) throws Throwable {
       
   158         String origin = args[0];
       
   159         System.out.println("Target: Testing original Host class from " + origin);
       
   160 
       
   161         // Make sure the Host class loaded directly is an original version
       
   162         // and from the expected location
       
   163         Host h = new Host();
       
   164         assertTrue(h.m() == 1);
       
   165         assertTrue(Host.getID().startsWith(origin + "/"));
       
   166 
       
   167         // The rest of this setup is only needed for the case
       
   168         // when we perform the checkNestHostChanges() test.
       
   169         if (origin.equals("HostA")) {
       
   170             String name = "Host$A";
       
   171 
       
   172             // Have to do this reflectively as there is no Host$A
       
   173             // when compiling the "Host/" case.
       
   174             Class<?> nestedA = Class.forName(name); // triggers initialization
       
   175 
       
   176             // This is compiled as a top-level class: the $ in the name is not
       
   177             // significant to the compiler.
       
   178             String hostA = "public class " + name + " {}";
       
   179             byte[] bytes = InMemoryJavaCompiler.compile(name, hostA);
       
   180             // And we have to load this into a new classloader
       
   181             Class<?> topLevelHostA = ByteCodeLoader.load(name, bytes);
       
   182             // The loaded class has not been linked (as per ClassLoader.resolveClass)
       
   183             // and so will be filtered out by VirtualMachine.allClasses(). There are
       
   184             // a number of ways to force linking - this is the simplest.
       
   185             Object o = topLevelHostA.newInstance();
       
   186 
       
   187             // sanity check
       
   188             assertTrue(nestedA.getClassLoader() != topLevelHostA.getClassLoader());
       
   189 
       
   190         }
       
   191 
       
   192         allowRedefine(); // debugger stops us here to attempt redefinitions
       
   193 
       
   194         System.out.println("Target executed okay");
       
   195     }
       
   196 
       
   197     public static void allowRedefine() { }
       
   198 }
       
   199 
       
   200 public class TestNestmateAttr extends TestScaffold {
       
   201 
       
   202     static final String SRC = System.getProperty("test.src");
       
   203     static final String DEST = System.getProperty("test.classes");
       
   204     static final boolean VERBOSE = Boolean.getBoolean("verbose");
       
   205 
       
   206     static String origin;
       
   207 
       
   208     // override this to correct a bug so arguments can be passed to
       
   209     // the Target class
       
   210     protected void startUp(String targetName) {
       
   211         List<String> argList = new ArrayList<>(Arrays.asList(args));
       
   212         argList.add(0, targetName); // pre-pend so it becomes the first "app" arg
       
   213         println("run args: " + argList);
       
   214         connect((String[]) argList.toArray(args));
       
   215         waitForVMStart();
       
   216     }
       
   217 
       
   218     TestNestmateAttr (String[] args) {
       
   219         super(args);
       
   220     }
       
   221 
       
   222     public static void main(String[] args) throws Throwable {
       
   223         origin = args[0];
       
   224         new TestNestmateAttr(args).startTests();
       
   225     }
       
   226 
       
   227     public void runTests() throws Exception {
       
   228         // Get Target into debuggable state
       
   229         BreakpointEvent bpe = startToMain("Target");
       
   230         EventRequestManager erm = vm().eventRequestManager();
       
   231         MethodEntryRequest mee = erm.createMethodEntryRequest();
       
   232         mee.addClassFilter("Target");
       
   233         mee.enable();
       
   234 
       
   235         // Allow application to complete and shut down
       
   236         listenUntilVMDisconnect();
       
   237 
       
   238         if (getExceptionCaught()) {
       
   239             throw new Exception("TestNestmateAttr: failed due to unexpected exception - check logs for details");
       
   240         }
       
   241         else if (!testFailed) {
       
   242             println("TestNestmateAttr: passed");
       
   243         } else {
       
   244             throw new Exception("TestNestmateAttr: failure reported - check log for details");
       
   245         }
       
   246     }
       
   247 
       
   248     // All the actual work is done from here once we see we've entered Target.allowRedefine()
       
   249     public void methodEntered(MethodEntryEvent event) {
       
   250         Method meth = event.location().method();
       
   251 
       
   252         if (!meth.name().equals("allowRedefine")) {
       
   253             return;
       
   254         }
       
   255 
       
   256         System.out.println("TestNestmateAttr: Testing original Host class from " + origin);
       
   257 
       
   258         String[] badTransforms;  // directories of bad classes
       
   259         String[] goodTransforms; // directories of good classes
       
   260 
       
   261         boolean testNestHostChanges = false;
       
   262 
       
   263         switch (origin) {
       
   264         case "Host":
       
   265             badTransforms = new String[] {
       
   266                 "HostA" // add member
       
   267             };
       
   268             goodTransforms = new String[] {
       
   269                 origin
       
   270             };
       
   271             break;
       
   272 
       
   273         case "HostA":
       
   274             badTransforms = new String[] {
       
   275                 "Host",   // remove member
       
   276                 "HostAB", // add member
       
   277                 "HostB"   // change member
       
   278             };
       
   279             goodTransforms = new String[] {
       
   280                 origin
       
   281             };
       
   282             testNestHostChanges = true;
       
   283             break;
       
   284 
       
   285         case "HostAB":
       
   286             badTransforms = new String[] {
       
   287                 "HostA",   // remove member
       
   288                 "HostABC", // add member
       
   289                 "HostAC"   // change member
       
   290             };
       
   291             goodTransforms = new String[] {
       
   292                 origin,
       
   293                 "HostBA"  // reorder members
       
   294             };
       
   295             break;
       
   296 
       
   297         case "HostABC":
       
   298             badTransforms = new String[] {
       
   299                 "HostAB",   // remove member
       
   300                 "HostABCD", // add member
       
   301                 "HostABD"   // change member
       
   302             };
       
   303             goodTransforms = new String[] {
       
   304                 origin,
       
   305                 "HostACB",  // reorder members
       
   306                 "HostBAC",  // reorder members
       
   307                 "HostBCA",  // reorder members
       
   308                 "HostCAB",  // reorder members
       
   309                 "HostCBA"   // reorder members
       
   310             };
       
   311             break;
       
   312 
       
   313         default: throw new Error("Unknown test directory: " + origin);
       
   314         }
       
   315 
       
   316         // Need to locate the type we will be trying to redefine in Target
       
   317         findReferenceTypes();
       
   318 
       
   319         try {
       
   320             // Compile and check bad transformations
       
   321             checkBadTransforms(_Host, badTransforms);
       
   322 
       
   323             // Compile and check good transformations
       
   324             checkGoodTransforms(_Host, goodTransforms);
       
   325 
       
   326             if (testNestHostChanges)
       
   327                 checkNestHostChanges();
       
   328         }
       
   329         catch (Throwable t) {
       
   330             failure(t);
       
   331         }
       
   332     }
       
   333 
       
   334     // override to give exception details
       
   335     protected void failure(Throwable t) {
       
   336         super.failure(t.getMessage());
       
   337         t.printStackTrace(System.out);
       
   338     }
       
   339 
       
   340     // These are references to the types in Target
       
   341     // that we will be trying to redefine.
       
   342     ReferenceType _Host;
       
   343     ReferenceType _Host_A_nested;
       
   344     ReferenceType _Host_A_topLevel;
       
   345 
       
   346     void findReferenceTypes() {
       
   347         List<ReferenceType> classes = vm().allClasses();
       
   348         ClassLoaderReference cl = null; // track the main loader
       
   349         ReferenceType a1 = null;
       
   350         ReferenceType a2 = null;
       
   351         for (ReferenceType c : classes) {
       
   352             String name = c.name();
       
   353             if (name.equals("Host")) {
       
   354                 _Host = c;
       
   355                 cl = c.classLoader();
       
   356             }
       
   357             else if (name.equals("Host$A")) {
       
   358                 if (a1 == null) {
       
   359                     a1 = c;
       
   360                 } else if (a2 == null) {
       
   361                     a2 = c;
       
   362                 }
       
   363                 else {
       
   364                     assertTrue(false); // Too many Host$A classes found!
       
   365                 }
       
   366             }
       
   367         }
       
   368         assertTrue(_Host != null);
       
   369 
       
   370         // The rest of this setup is only needed for the case
       
   371         // when we perform the checkNestHostChanges() test.
       
   372         if (origin.equals("HostA")) {
       
   373             assertTrue(a1 != null);
       
   374             assertTrue(a2 != null);
       
   375 
       
   376             if (a1.classLoader() == cl) {
       
   377                 _Host_A_nested = a1;
       
   378                 assertTrue(a2.classLoader() != cl);
       
   379                 _Host_A_topLevel = a2;
       
   380             }
       
   381             else if (a2.classLoader() == cl) {
       
   382                 _Host_A_nested = a2;
       
   383                 assertTrue(a1.classLoader() != cl);
       
   384                 _Host_A_topLevel = a1;
       
   385             }
       
   386             else {
       
   387                 assertTrue(false); // Wrong classLoaders found
       
   388             }
       
   389         }
       
   390     }
       
   391 
       
   392     void checkNestHostChanges() throws Throwable {
       
   393         Map<ReferenceType, byte[]> map = new HashMap<>();
       
   394 
       
   395         // case 1: remove NestHost attribute
       
   396         //   - try to redefine nested Host$A with a top-level
       
   397         //     class called Host$A
       
   398         System.out.println("Trying bad retransform that removes the NestHost attribute");
       
   399 
       
   400         String name = "Host$A";
       
   401 
       
   402         // This is compiled as a top-level class: the $ in the name is not
       
   403         // significant to the compiler.
       
   404         String hostA = "public class " + name + " {}";
       
   405         byte[] bytes = InMemoryJavaCompiler.compile(name, hostA);
       
   406 
       
   407         map.put(_Host_A_nested, bytes);
       
   408 
       
   409         try {
       
   410             vm().redefineClasses(map);
       
   411             throw new Error("Retransformation to top-level class " + name +
       
   412                             " succeeded unexpectedly");
       
   413         }
       
   414         catch (UnsupportedOperationException uoe) {
       
   415             if (uoe.getMessage().contains("changes to class attribute not implemented")) {
       
   416                 System.out.println("Got expected exception " + uoe);
       
   417             }
       
   418             else throw new Error("Wrong UnsupportedOperationException", uoe);
       
   419         }
       
   420 
       
   421         map.clear();
       
   422 
       
   423         // case 2: add NestHost attribute
       
   424         //  - This is tricky because the class with no NestHost attribute
       
   425         //    has to have the name of a nested class! But we know how to
       
   426         //    do that as we already created a top-level Host$A. So now
       
   427         //    we try to replace with a really nested Host$A.
       
   428 
       
   429         System.out.println("Trying bad retransform that adds the NestHost attribute");
       
   430 
       
   431         byte[] nestedBytes;
       
   432         File clsfile = new File(DEST + "/" + name + ".class");
       
   433         if (VERBOSE) System.out.println("Reading bytes from " + clsfile);
       
   434         try (FileInputStream str = new FileInputStream(clsfile)) {
       
   435             nestedBytes = NamedBuffer.loadBufferFromStream(str);
       
   436         }
       
   437 
       
   438         map.put(_Host_A_topLevel, nestedBytes);
       
   439 
       
   440         try {
       
   441             vm().redefineClasses(map);
       
   442             throw new Error("Retransformation to nested class " + name +
       
   443                             " succeeded unexpectedly");
       
   444         }
       
   445         catch (UnsupportedOperationException uoe) {
       
   446             if (uoe.getMessage().contains("changes to class attribute not implemented")) {
       
   447                 System.out.println("Got expected exception " + uoe);
       
   448             }
       
   449             else throw new Error("Wrong UnsupportedOperationException", uoe);
       
   450         }
       
   451 
       
   452         map.clear();
       
   453 
       
   454         // case 3: replace the NestHost attribute
       
   455         //  - the easiest way (perhaps only reasonable way) to do this
       
   456         //    is to search for the Utf8 entry used by the Constant_ClassRef,
       
   457         //    set in the NestHost attribute, and edit it to refer to a different
       
   458         //    name. We reuse nestedBytes from above.
       
   459 
       
   460         System.out.println("Trying bad retransform that changes the NestHost attribute");
       
   461 
       
   462         int utf8Entry_length = 7;
       
   463         boolean found = false;
       
   464         for (int i = 0; i < nestedBytes.length - utf8Entry_length; i++) {
       
   465             if (nestedBytes[i] == 1 &&   // utf8 tag
       
   466                 nestedBytes[i+1] == 0 && // msb of length
       
   467                 nestedBytes[i+2] == 4 && // lsb of length
       
   468                 nestedBytes[i+3] == (byte) 'H' &&
       
   469                 nestedBytes[i+4] == (byte) 'o' &&
       
   470                 nestedBytes[i+5] == (byte) 's' &&
       
   471                 nestedBytes[i+6] == (byte) 't') {
       
   472 
       
   473                 if (VERBOSE) System.out.println("Appear to have found Host utf8 entry starting at " + i);
       
   474 
       
   475                 nestedBytes[i+3] = (byte) 'G';
       
   476                 found = true;
       
   477                 break;
       
   478             }
       
   479         }
       
   480 
       
   481         if (!found)
       
   482             throw new Error("Could not locate 'Host' name in byte array");
       
   483 
       
   484         map.put(_Host_A_nested, nestedBytes);
       
   485 
       
   486         try {
       
   487             vm().redefineClasses(map);
       
   488             throw new Error("Retransformation to modified nested class" +
       
   489                             " succeeded unexpectedly");
       
   490         }
       
   491         catch (UnsupportedOperationException uoe) {
       
   492             if (uoe.getMessage().contains("changes to class attribute not implemented")) {
       
   493                 System.out.println("Got expected exception " + uoe);
       
   494             }
       
   495             else throw new Error("Wrong UnsupportedOperationException", uoe);
       
   496         }
       
   497 
       
   498     }
       
   499 
       
   500     void checkGoodTransforms(ReferenceType c, String[] dirs) throws Throwable {
       
   501         // To verify the redefinition actually took place we will invoke the
       
   502         // Host.getID method and check the result. To do that we need to find the
       
   503         // main thread in the target VM. We don't check that "(new Host()).m()"
       
   504         // returns 2 due to the complexity of setting that up via JDI.
       
   505 
       
   506         ThreadReference main = null;
       
   507         List<ThreadReference> threads = vm().allThreads();
       
   508         for (ThreadReference t : threads) {
       
   509             if (t.name().equals("main")) {
       
   510                 main = t;
       
   511                 break;
       
   512             }
       
   513         }
       
   514 
       
   515         assertTrue(main != null);
       
   516 
       
   517         // Now find the method
       
   518         Method getID = null;
       
   519         List<Method> methods = _Host.methodsByName("getID");
       
   520         assertTrue(methods.size() == 1);
       
   521         getID = methods.get(0);
       
   522 
       
   523         Map<ReferenceType, byte[]> map = new HashMap<>();
       
   524         for (String dir : dirs) {
       
   525             dir += "/redef";
       
   526             System.out.println("Trying good retransform from " + dir);
       
   527             byte[] buf = bytesForHostClass(dir);
       
   528             map.put(c, buf);
       
   529             vm().redefineClasses(map);
       
   530             map.clear();
       
   531             // Test redefinition worked
       
   532             Value v = ((ClassType)_Host).invokeMethod(main, getID, Collections.emptyList(), 0);
       
   533             assertTrue(v instanceof StringReference);
       
   534             String id =  ((StringReference)v).value();
       
   535             if (VERBOSE) System.out.println("Redefined ID: " + id);
       
   536             assertTrue(id.startsWith(dir));
       
   537             assertTrue(id.contains("/redef/"));
       
   538         }
       
   539     }
       
   540 
       
   541     void checkBadTransforms(ReferenceType c, String[] dirs) throws Throwable {
       
   542         Map<ReferenceType, byte[]> map = new HashMap<>();
       
   543         for (String dir : dirs) {
       
   544             dir += "/redef";
       
   545             System.out.println("Trying bad retransform from " + dir);
       
   546             byte[] buf = bytesForHostClass(dir);
       
   547             map.put(c, buf);
       
   548             try {
       
   549                 vm().redefineClasses(map);
       
   550                 throw new Error("Retransformation from directory " + dir +
       
   551                                 " succeeded unexpectedly");
       
   552             }
       
   553             catch (UnsupportedOperationException uoe) {
       
   554                 if (uoe.getMessage().contains("changes to class attribute not implemented")) {
       
   555                     System.out.println("Got expected exception " + uoe);
       
   556                 }
       
   557                 else throw new Error("Wrong UnsupportedOperationException", uoe);
       
   558             }
       
   559         }
       
   560     }
       
   561 
       
   562     static byte[] bytesForHostClass(String dir) throws Throwable {
       
   563         compile("/" + dir);
       
   564         File clsfile = new File(DEST + "/" + dir + "/Host.class");
       
   565         if (VERBOSE) System.out.println("Reading bytes from " + clsfile);
       
   566         byte[] buf = null;
       
   567         try (FileInputStream str = new FileInputStream(clsfile)) {
       
   568             return buf = NamedBuffer.loadBufferFromStream(str);
       
   569         }
       
   570     }
       
   571 
       
   572     static void compile(String dir) throws Throwable {
       
   573         File src = new File(SRC + dir);
       
   574         File dst = new File(DEST + dir);
       
   575         if (VERBOSE) System.out.println("Compiling from: " + src + "\n" +
       
   576                                         "            to: " + dst);
       
   577         CompilerUtils.compile(src.toPath(),
       
   578                               dst.toPath(),
       
   579                               false /* don't recurse */,
       
   580                               new String[0]);
       
   581     }
       
   582 }