view test/java/lang/module/AutomaticModulesTest.java @ 17234:f8b19df2115a

8181087: Module system implementation refresh (6/2017) Reviewed-by: plevart, mchung Contributed-by: alan.bateman@oracle.com, alex.buckley@oracle.com
author alanb
date Fri, 16 Jun 2017 09:20:39 -0700
parents 73113c19a5df
children
line wrap: on
line source

/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/**
 * @test
 * @library /lib/testlibrary
 * @build AutomaticModulesTest ModuleUtils JarUtils
 * @run testng AutomaticModulesTest
 * @summary Basic tests for automatic modules
 */

import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.FindException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires.Modifier;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolutionException;
import java.lang.module.ResolvedModule;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;

@Test
public class AutomaticModulesTest {

    private static final Path USER_DIR
         = Paths.get(System.getProperty("user.dir"));

    @DataProvider(name = "jarnames")
    public Object[][] createJarNames() {
        return new Object[][] {

            // JAR file name                module-name[/version]

            { "foo.jar",                    "foo" },
            { "foo4j.jar",                  "foo4j", },

            { "foo1.jar",                   "foo1" },
            { "foo10.jar",                  "foo10" },

            { "foo-1.jar",                  "foo/1" },
            { "foo-1.2.jar",                "foo/1.2" },
            { "foo-1.2.3.jar",              "foo/1.2.3" },
            { "foo-1.2.3.4.jar",            "foo/1.2.3.4" },

            { "foo-10.jar",                 "foo/10" },
            { "foo-10.20.jar",              "foo/10.20" },
            { "foo-10.20.30.jar",           "foo/10.20.30" },
            { "foo-10.20.30.40.jar",        "foo/10.20.30.40" },

            { "foo-bar.jar",                "foo.bar" },
            { "foo-bar-1.jar",              "foo.bar/1" },
            { "foo-bar-1.2.jar",            "foo.bar/1.2"},
            { "foo-bar-10.jar",             "foo.bar/10" },
            { "foo-bar-10.20.jar",          "foo.bar/10.20" },

            { "foo.bar1.jar",               "foo.bar1" },
            { "foo.bar10.jar",              "foo.bar10" },

            { "foo-1.2-SNAPSHOT.jar",       "foo/1.2-SNAPSHOT" },
            { "foo-bar-1.2-SNAPSHOT.jar",   "foo.bar/1.2-SNAPSHOT" },

            { "foo--bar-1.0.jar",           "foo.bar/1.0" },
            { "-foo-bar-1.0.jar",           "foo.bar/1.0" },
            { "foo-bar--1.0.jar",           "foo.bar/1.0" },

        };
    }

    // JAR file names that do not map to a legal module name
    @DataProvider(name = "badjarnames")
    public Object[][] createBadNames() {
        return new Object[][]{

            { ".jar",          null },
            { "_.jar",         null },

            { "foo.1.jar",     null },
            { "1foo.jar",      null },
            { "foo.1bar.jar",  null },

        };
    }

    /**
     * Test mapping of JAR file names to module names
     */
    @Test(dataProvider = "jarnames")
    public void testNames(String fn, String mid) throws IOException {
        String[] s = mid.split("/");
        String mn = s[0];
        String vs = (s.length == 2) ? s[1] : null;

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        Path jf = dir.resolve(fn);

        // create empty JAR file
        createDummyJarFile(jf);

        // create a ModuleFinder to find modules in the directory
        ModuleFinder finder = ModuleFinder.of(dir);

        // a module with the expected name should be found
        Optional<ModuleReference> mref = finder.find(mn);
        assertTrue(mref.isPresent(), mn + " not found");

        ModuleDescriptor descriptor = mref.get().descriptor();
        assertTrue(descriptor.isAutomatic());
        assertEquals(descriptor.name(), mn);
        if (vs == null) {
            assertFalse(descriptor.version().isPresent());
        } else {
            assertEquals(descriptor.version().get().toString(), vs);
        }
    }

    /**
     * Test impossible mapping of JAR files to modules names
     */
    @Test(dataProvider = "badjarnames", expectedExceptions = FindException.class)
    public void testBadNames(String fn, String ignore) throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        Path jf = dir.resolve(fn);

        // create empty JAR file
        createDummyJarFile(jf);

        // should throw FindException
        ModuleFinder.of(dir).findAll();
    }


    @DataProvider(name = "modulenames")
    public Object[][] createModuleNames() {
        return new Object[][] {
            { "foo",        null },
            { "foo",        "1.0" },
            { "foo.bar",    null },
            { "foo.bar",    "1.0" },
            { "class_",     null },
            { "class_",     "1.0" },
        };
    }

    @DataProvider(name = "badmodulenames")
    public Object[][] createBadModuleNames() {
        return new Object[][] {
            { "",            null },
            { "",            "1.0" },
            { "666",         null },
            { "666",         "1.0" },
            { "foo.class",   null },
            { "foo.class",   "1.0" },
        };
    }

    /**
     * Test JAR files with the Automatic-Module-Name attribute
     */
    @Test(dataProvider = "modulenames")
    public void testAutomaticModuleNameAttribute(String name, String vs)
        throws IOException
    {
        Manifest man = new Manifest();
        Attributes attrs = man.getMainAttributes();
        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
        attrs.put(new Attributes.Name("Automatic-Module-Name"), name);

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        String jar;
        if (vs == null) {
            jar = "m.jar";
        } else {
            jar = "m-" + vs + ".jar";
        }
        createDummyJarFile(dir.resolve(jar), man);

        ModuleFinder finder = ModuleFinder.of(dir);

        assertTrue(finder.findAll().size() == 1);
        assertTrue(finder.find(name).isPresent());

        ModuleReference mref = finder.find(name).get();
        ModuleDescriptor descriptor = mref.descriptor();
        assertEquals(descriptor.name(), name);
        assertEquals(descriptor.version()
                .map(ModuleDescriptor.Version::toString)
                .orElse(null), vs);
    }

    /**
     * Test JAR files with the Automatic-Module-Name attribute with a value
     * that is not a legal module name.
     */
    @Test(dataProvider = "badmodulenames", expectedExceptions = FindException.class)
    public void testBadAutomaticModuleNameAttribute(String name, String ignore)
        throws IOException
    {
        // should throw FindException
        testAutomaticModuleNameAttribute(name, null);
    }

    /**
     * Test all packages are exported
     */
    public void testPackages() throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("m.jar"),
                           "p/C1.class", "p/C2.class", "q/C1.class");

        ModuleFinder finder = ModuleFinder.of(dir);
        Optional<ModuleReference> mref = finder.find("m");
        assertTrue(mref.isPresent(), "m not found");

        ModuleDescriptor descriptor = mref.get().descriptor();
        assertTrue(descriptor.isAutomatic());

        assertTrue(descriptor.packages().size() == 2);
        assertTrue(descriptor.packages().contains("p"));
        assertTrue(descriptor.packages().contains("q"));

        assertTrue(descriptor.exports().isEmpty());
        assertTrue(descriptor.opens().isEmpty());
    }

    /**
     * Test class files in JAR file where the entry does not correspond to a
     * legal package name.
     */
    public void testBadPackage() throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("m.jar"), "p/C1.class", "p-/C2.class");

        ModuleFinder finder = ModuleFinder.of(dir);
        Optional<ModuleReference> mref = finder.find("m");
        assertTrue(mref.isPresent(), "m not found");

        ModuleDescriptor descriptor = mref.get().descriptor();
        assertTrue(descriptor.isAutomatic());

        assertTrue(descriptor.packages().size() == 1);
        assertTrue(descriptor.packages().contains("p"));

        assertTrue(descriptor.exports().isEmpty());
        assertTrue(descriptor.opens().isEmpty());
    }

    /**
     * Test non-class resources in a JAR file.
     */
    public void testNonClassResources() throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("m.jar"),
                "LICENSE",
                "README",
                "WEB-INF/tags",
                "p/Type.class",
                "p/resources/m.properties");

        ModuleFinder finder = ModuleFinder.of(dir);
        Optional<ModuleReference> mref = finder.find("m");
        assertTrue(mref.isPresent(), "m not found");

        ModuleDescriptor descriptor = mref.get().descriptor();
        assertTrue(descriptor.isAutomatic());

        assertTrue(descriptor.packages().size() == 1);
        assertTrue(descriptor.packages().contains("p"));
    }

    /**
     * Test .class file in unnamed package (top-level directory)
     */
    @Test(expectedExceptions = FindException.class)
    public void testClassInUnnamedPackage() throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("m.jar"), "Mojo.class");
        ModuleFinder finder = ModuleFinder.of(dir);
        finder.findAll();
    }

    /**
     * Test JAR file with META-INF/services configuration file
     */
    public void testServicesConfiguration() throws IOException {
        String service = "p.S";
        String provider = "p.S1";

        Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp");

        // provider class
        Path providerClass = tmpdir.resolve(provider.replace('.', '/') + ".class");
        Files.createDirectories(providerClass.getParent());
        Files.createFile(providerClass);

        // services configuration file
        Path services = tmpdir.resolve("META-INF").resolve("services");
        Files.createDirectories(services);
        Files.write(services.resolve(service), Set.of(provider));

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir);

        ModuleFinder finder = ModuleFinder.of(dir);

        Optional<ModuleReference> mref = finder.find("m");
        assertTrue(mref.isPresent(), "m not found");

        ModuleDescriptor descriptor = mref.get().descriptor();
        assertTrue(descriptor.provides().size() == 1);
        ModuleDescriptor.Provides provides = descriptor.provides().iterator().next();
        assertEquals(provides.service(), service);
        assertTrue(provides.providers().size() == 1);
        assertTrue(provides.providers().contains((provider)));
    }

    // META-INF/services files that don't map to legal service names
    @DataProvider(name = "badservices")
    public Object[][] createBadServices() {
        return new Object[][] {

                // service type         provider type
                { "-",                  "p.S1" },
                { ".S",                 "p.S1" },
        };
    }

    /**
     * Test JAR file with META-INF/services configuration file with bad
     * values or names.
     */
    @Test(dataProvider = "badservices")
    public void testBadServicesNames(String service, String provider)
        throws IOException
    {
        Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp");
        Path services = tmpdir.resolve("META-INF").resolve("services");
        Files.createDirectories(services);
        Files.write(services.resolve(service), Set.of(provider));
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir);

        Optional<ModuleReference> omref = ModuleFinder.of(dir).find("m");
        assertTrue(omref.isPresent());
        ModuleDescriptor descriptor = omref.get().descriptor();
        assertTrue(descriptor.provides().isEmpty());
    }

    // META-INF/services configuration file entries that are not legal
    @DataProvider(name = "badproviders")
    public Object[][] createBadProviders() {
        return new Object[][] {

                // service type         provider type
                { "p.S",                "-" },
                { "p.S",                "p..S1" },
                { "p.S",                "S1." },
        };
    }

    /**
     * Test JAR file with META-INF/services configuration file with bad
     * values or names.
     */
    @Test(dataProvider = "badproviders", expectedExceptions = FindException.class)
    public void testBadProviderNames(String service, String provider)
        throws IOException
    {
        Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp");

        // provider class
        Path providerClass = tmpdir.resolve(provider.replace('.', '/') + ".class");
        Files.createDirectories(providerClass.getParent());
        Files.createFile(providerClass);

        // services configuration file
        Path services = tmpdir.resolve("META-INF").resolve("services");
        Files.createDirectories(services);
        Files.write(services.resolve(service), Set.of(provider));

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir);

        // should throw FindException
        ModuleFinder.of(dir).findAll();
    }

    /**
     * Test JAR file with META-INF/services configuration file listing a
     * provider that is not in the module.
     */
    @Test(expectedExceptions = FindException.class)
    public void testMissingProviderPackage() throws IOException {
        Path tmpdir = Files.createTempDirectory(USER_DIR, "tmp");

        // services configuration file
        Path services = tmpdir.resolve("META-INF").resolve("services");
        Files.createDirectories(services);
        Files.write(services.resolve("p.S"), Set.of("q.P"));

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        JarUtils.createJarFile(dir.resolve("m.jar"), tmpdir);

        // should throw FindException
        ModuleFinder.of(dir).findAll();
    }

    /**
     * Test that a JAR file with a Main-Class attribute results
     * in a module with a main class.
     */
    public void testMainClass() throws IOException {
        String mainClass = "p.Main";

        Manifest man = new Manifest();
        Attributes attrs = man.getMainAttributes();
        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
        attrs.put(Attributes.Name.MAIN_CLASS, mainClass);

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        String entry = mainClass.replace('.', '/') + ".class";
        createDummyJarFile(dir.resolve("m.jar"), man, entry);

        ModuleFinder finder = ModuleFinder.of(dir);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "m");

        ModuleDescriptor descriptor = findDescriptor(cf, "m");

        assertTrue(descriptor.mainClass().isPresent());
        assertEquals(descriptor.mainClass().get(), mainClass);
    }

    // Main-Class files that do not map to a legal qualified type name
    @DataProvider(name = "badmainclass")
    public Object[][] createBadMainClass() {
        return new Object[][] {
            { "p..Main",     null },
            { "p-.Main",     null },

        };
    }

    /**
     * Test that a JAR file with a Main-Class attribute that is not a qualified
     * type name.
     */
    @Test(dataProvider = "badmainclass")
    public void testBadMainClass(String mainClass, String ignore) throws IOException {
        Manifest man = new Manifest();
        Attributes attrs = man.getMainAttributes();
        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
        attrs.put(Attributes.Name.MAIN_CLASS, mainClass);

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        String entry = mainClass.replace('.', '/') + ".class";
        createDummyJarFile(dir.resolve("m.jar"), man, entry);

        // bad Main-Class value should be ignored
        Optional<ModuleReference> omref = ModuleFinder.of(dir).find("m");
        assertTrue(omref.isPresent());
        ModuleDescriptor descriptor = omref.get().descriptor();
        assertFalse(descriptor.mainClass().isPresent());
    }

    /**
     * Test that a JAR file with a Main-Class attribute that is not in the module
     */
    public void testMissingMainClassPackage() throws IOException {
        Manifest man = new Manifest();
        Attributes attrs = man.getMainAttributes();
        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
        attrs.put(Attributes.Name.MAIN_CLASS, "p.Main");

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("m.jar"), man);

        // Main-Class should be ignored because package p is not in module
        Optional<ModuleReference> omref = ModuleFinder.of(dir).find("m");
        assertTrue(omref.isPresent());
        ModuleDescriptor descriptor = omref.get().descriptor();
        assertFalse(descriptor.mainClass().isPresent());
    }

    /**
     * Basic test of a configuration created with automatic modules.
     *   a requires b*
     *   a requires c*
     *   b*
     *   c*
     */
    public void testConfiguration1() throws Exception {
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("a")
                .requires("b")
                .requires("c")
                .requires("java.base")
                .build();

        // b and c are automatic modules
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("b.jar"), "p/T.class");
        createDummyJarFile(dir.resolve("c.jar"), "q/T.class");

        // module finder locates a and the modules in the directory
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1);
        ModuleFinder finder2 = ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "a");

        assertTrue(cf.modules().size() == 3);
        assertTrue(cf.findModule("a").isPresent());
        assertTrue(cf.findModule("b").isPresent());
        assertTrue(cf.findModule("c").isPresent());

        ResolvedModule base = cf.findModule("java.base").get();
        assertTrue(base.configuration() == ModuleLayer.boot().configuration());
        ResolvedModule a = cf.findModule("a").get();
        ResolvedModule b = cf.findModule("b").get();
        ResolvedModule c = cf.findModule("c").get();

        // b && c only require java.base
        assertTrue(b.reference().descriptor().requires().size() == 1);
        assertTrue(c.reference().descriptor().requires().size() == 1);

        // readability

        assertTrue(a.reads().size() == 3);
        assertTrue(a.reads().contains(base));
        assertTrue(a.reads().contains(b));
        assertTrue(a.reads().contains(c));

        assertTrue(b.reads().contains(a));
        assertTrue(b.reads().contains(c));
        testReadAllBootModules(cf, "b");  // b reads all modules in boot layer

        assertTrue(c.reads().contains(a));
        assertTrue(c.reads().contains(b));
        testReadAllBootModules(cf, "c");  // c reads all modules in boot layer

    }

    /**
     * Basic test of a configuration created with automatic modules
     *   a requires b
     *   b requires c*
     *   c*
     *   d*
     */
    public void testInConfiguration2() throws IOException {
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("a")
                .requires("b")
                .requires("java.base")
                .build();

        ModuleDescriptor descriptor2
            = ModuleDescriptor.newModule("b")
                .requires("c")
                .requires("java.base")
                .build();

        // c and d are automatic modules
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("c.jar"), "p/T.class");
        createDummyJarFile(dir.resolve("d.jar"), "q/T.class");

        // module finder locates a and the modules in the directory
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1, descriptor2);
        ModuleFinder finder2 = ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "a", "d");

        assertTrue(cf.modules().size() == 4);
        assertTrue(cf.findModule("a").isPresent());
        assertTrue(cf.findModule("b").isPresent());
        assertTrue(cf.findModule("c").isPresent());
        assertTrue(cf.findModule("d").isPresent());

        // c && d should only require java.base
        assertTrue(findDescriptor(cf, "c").requires().size() == 1);
        assertTrue(findDescriptor(cf, "d").requires().size() == 1);

        // readability

        ResolvedModule base = cf.findModule("java.base").get();
        assertTrue(base.configuration() == ModuleLayer.boot().configuration());
        ResolvedModule a = cf.findModule("a").get();
        ResolvedModule b = cf.findModule("b").get();
        ResolvedModule c = cf.findModule("c").get();
        ResolvedModule d = cf.findModule("d").get();

        assertTrue(a.reads().size() == 2);
        assertTrue(a.reads().contains(b));
        assertTrue(a.reads().contains(base));

        assertTrue(b.reads().size() == 3);
        assertTrue(b.reads().contains(c));
        assertTrue(b.reads().contains(d));
        assertTrue(b.reads().contains(base));

        assertTrue(c.reads().contains(a));
        assertTrue(c.reads().contains(b));
        assertTrue(c.reads().contains(d));
        testReadAllBootModules(cf, "c");   // c reads all modules in boot layer

        assertTrue(d.reads().contains(a));
        assertTrue(d.reads().contains(b));
        assertTrue(d.reads().contains(c));
        testReadAllBootModules(cf, "d");    // d reads all modules in boot layer
    }

    /**
     * Basic test of a configuration created with automatic modules
     *   a requires b
     *   b requires transitive c*
     *   c*
     *   d*
     */
    public void testInConfiguration3() throws IOException {
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("a")
                .requires("b")
                .requires("java.base")
                .build();

        ModuleDescriptor descriptor2
            = ModuleDescriptor.newModule("b")
                .requires(Set.of(Modifier.TRANSITIVE), "c")
                .requires("java.base")
                .build();

        // c and d are automatic modules
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("c.jar"), "p/T.class");
        createDummyJarFile(dir.resolve("d.jar"), "q/T.class");

        // module finder locates a and the modules in the directory
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1, descriptor2);
        ModuleFinder finder2 = ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "a", "d");

        assertTrue(cf.modules().size() == 4);
        assertTrue(cf.findModule("a").isPresent());
        assertTrue(cf.findModule("b").isPresent());
        assertTrue(cf.findModule("c").isPresent());
        assertTrue(cf.findModule("d").isPresent());

        ResolvedModule base = cf.findModule("java.base").get();
        assertTrue(base.configuration() == ModuleLayer.boot().configuration());
        ResolvedModule a = cf.findModule("a").get();
        ResolvedModule b = cf.findModule("b").get();
        ResolvedModule c = cf.findModule("c").get();
        ResolvedModule d = cf.findModule("d").get();

        // c && d should only require java.base
        assertTrue(findDescriptor(cf, "c").requires().size() == 1);
        assertTrue(findDescriptor(cf, "d").requires().size() == 1);

        // readability

        assertTrue(a.reads().size() == 4);
        assertTrue(a.reads().contains(b));
        assertTrue(a.reads().contains(c));
        assertTrue(a.reads().contains(d));
        assertTrue(a.reads().contains(base));

        assertTrue(b.reads().size() == 3);
        assertTrue(b.reads().contains(c));
        assertTrue(b.reads().contains(d));
        assertTrue(b.reads().contains(base));

        assertTrue(reads(cf, "b", "c"));
        assertTrue(reads(cf, "b", "d"));
        assertTrue(reads(cf, "b", "java.base"));

        assertTrue(c.reads().contains(a));
        assertTrue(c.reads().contains(b));
        assertTrue(c.reads().contains(d));
        testReadAllBootModules(cf, "c");   // c reads all modules in boot layer

        assertTrue(d.reads().contains(a));
        assertTrue(d.reads().contains(b));
        assertTrue(d.reads().contains(c));
        testReadAllBootModules(cf, "d");    // d reads all modules in boot layer
    }

    /**
     * Basic test to ensure that no automatic modules are resolved when
     * an automatic module is not a root or required by other modules.
     */
    public void testInConfiguration4() throws IOException {
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("m1")
                .requires("java.base")
                .build();

        // automatic modules
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("auto1.jar"), "p1/C.class");
        createDummyJarFile(dir.resolve("auto2.jar"), "p2/C.class");
        createDummyJarFile(dir.resolve("auto3.jar"), "p3/C.class");

        // module finder locates m1 and the modules in the directory
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1);
        ModuleFinder finder2 =  ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "m1");

        // ensure that no automatic module is resolved
        assertTrue(cf.modules().size() == 1);
        assertTrue(cf.findModule("m1").isPresent());
    }

    /**
     * Basic test to ensure that if an automatic module is resolved then
     * all observable automatic modules are resolved.
     */
    public void testInConfiguration5() throws IOException {
        // m1 requires m2
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("m1")
                .requires("m2").build();

        // m2 requires automatic module
        ModuleDescriptor descriptor2
            = ModuleDescriptor.newModule("m2")
                .requires("auto1")
                .build();

        // automatic modules
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("auto1.jar"), "p1/C.class");
        createDummyJarFile(dir.resolve("auto2.jar"), "p2/C.class");
        createDummyJarFile(dir.resolve("auto3.jar"), "p3/C.class");

        // module finder locates m1, m2, and the modules in the directory
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1, descriptor2);
        ModuleFinder finder2 =  ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "m1");

        // all automatic modules should be resolved
        assertTrue(cf.modules().size() == 5);
        assertTrue(cf.findModule("m1").isPresent());
        assertTrue(cf.findModule("m2").isPresent());
        assertTrue(cf.findModule("auto1").isPresent());
        assertTrue(cf.findModule("auto2").isPresent());
        assertTrue(cf.findModule("auto3").isPresent());

        ResolvedModule base = parent.findModule("java.base")
                                    .orElseThrow(() -> new RuntimeException());
        ResolvedModule m1 = cf.findModule("m1").get();
        ResolvedModule m2 = cf.findModule("m2").get();
        ResolvedModule auto1 = cf.findModule("auto1").get();
        ResolvedModule auto2 = cf.findModule("auto2").get();
        ResolvedModule auto3 = cf.findModule("auto3").get();

        // m1 does not read the automatic modules
        assertTrue(m1.reads().size() == 2);
        assertTrue(m1.reads().contains(m2));
        assertTrue(m1.reads().contains(base));

        // m2 should read all the automatic modules
        assertTrue(m2.reads().size() == 4);
        assertTrue(m2.reads().contains(auto1));
        assertTrue(m2.reads().contains(auto2));
        assertTrue(m2.reads().contains(auto3));
        assertTrue(m2.reads().contains(base));

        assertTrue(auto1.reads().contains(m1));
        assertTrue(auto1.reads().contains(m2));
        assertTrue(auto1.reads().contains(auto2));
        assertTrue(auto1.reads().contains(auto3));
        assertTrue(auto1.reads().contains(base));

        assertTrue(auto2.reads().contains(m1));
        assertTrue(auto2.reads().contains(m2));
        assertTrue(auto2.reads().contains(auto1));
        assertTrue(auto2.reads().contains(auto3));
        assertTrue(auto2.reads().contains(base));

        assertTrue(auto3.reads().contains(m1));
        assertTrue(auto3.reads().contains(m2));
        assertTrue(auto3.reads().contains(auto1));
        assertTrue(auto3.reads().contains(auto2));
        assertTrue(auto3.reads().contains(base));
    }

    /**
     * Basic test of automatic modules in a child configuration. All automatic
     * modules that are found with the before finder should be resolved. The
     * automatic modules that are found by the after finder and not shadowed
     * by the before finder, or parent configurations, should also be resolved.
     */
    public void testInConfiguration6() throws IOException {
        // m1 requires auto1
        ModuleDescriptor descriptor1
            = ModuleDescriptor.newModule("m1")
                .requires("auto1")
                .build();

        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("auto1.jar"), "p1/C.class");

        // module finder locates m1 and auto1
        ModuleFinder finder1 = ModuleUtils.finderOf(descriptor1);
        ModuleFinder finder2 =  ModuleFinder.of(dir);
        ModuleFinder finder = ModuleFinder.compose(finder1, finder2);

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf1 = resolve(parent, finder, "m1");

        assertTrue(cf1.modules().size() == 2);
        assertTrue(cf1.findModule("m1").isPresent());
        assertTrue(cf1.findModule("auto1").isPresent());

        ResolvedModule base = parent.findModule("java.base")
                                    .orElseThrow(() -> new RuntimeException());
        ResolvedModule m1 = cf1.findModule("m1").get();
        ResolvedModule auto1 = cf1.findModule("auto1").get();

        assertTrue(m1.reads().size() == 2);
        assertTrue(m1.reads().contains(auto1));
        assertTrue(m1.reads().contains(base));

        assertTrue(auto1.reads().contains(m1));
        assertTrue(auto1.reads().contains(base));


        // create child configuration - the after finder locates auto1

        dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("auto2.jar"), "p2/C.class");
        ModuleFinder beforeFinder =  ModuleFinder.of(dir);

        dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("auto1.jar"), "p1/C.class");
        createDummyJarFile(dir.resolve("auto2.jar"), "p2/C.class");
        createDummyJarFile(dir.resolve("auto3.jar"), "p3/C.class");
        ModuleFinder afterFinder =  ModuleFinder.of(dir);

        Configuration cf2 = cf1.resolve(beforeFinder, afterFinder, Set.of("auto2"));

        // auto1 should be found in parent and should not be in cf2
        assertTrue(cf2.modules().size() == 2);
        assertTrue(cf2.findModule("auto2").isPresent());
        assertTrue(cf2.findModule("auto3").isPresent());

        ResolvedModule auto2 = cf2.findModule("auto2").get();
        ResolvedModule auto3 = cf2.findModule("auto3").get();

        assertTrue(auto2.reads().contains(m1));
        assertTrue(auto2.reads().contains(auto1));
        assertTrue(auto2.reads().contains(auto3));
        assertTrue(auto2.reads().contains(base));

        assertTrue(auto3.reads().contains(m1));
        assertTrue(auto3.reads().contains(auto1));
        assertTrue(auto3.reads().contains(auto2));
        assertTrue(auto3.reads().contains(base));
    }

    /**
     * Basic test of a configuration created with automatic modules
     *   a requires b* and c*
     *   b* contains p
     *   c* contains p
     */
    @Test(expectedExceptions = { ResolutionException.class })
    public void testDuplicateSuppliers1() throws IOException {
        ModuleDescriptor descriptor
            = ModuleDescriptor.newModule("a")
                .requires("b")
                .requires("c")
                .build();

        // c and d are automatic modules with the same package
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("b.jar"), "p/T.class");
        createDummyJarFile(dir.resolve("c.jar"), "p/T.class");

        // module finder locates 'a' and the modules in the directory
        ModuleFinder finder
            = ModuleFinder.compose(ModuleUtils.finderOf(descriptor),
                                   ModuleFinder.of(dir));

        Configuration parent = ModuleLayer.boot().configuration();
        resolve(parent, finder, "a");
    }

    /**
     * Basic test of a configuration created with automatic modules
     *   a contains p, requires b*
     *   b* contains p
     */
    @Test(expectedExceptions = { ResolutionException.class })
    public void testDuplicateSuppliers2() throws IOException {
        ModuleDescriptor descriptor
            = ModuleDescriptor.newModule("a")
                .packages(Set.of("p"))
                .requires("b")
                .build();

        // c and d are automatic modules with the same package
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("b.jar"), "p/T.class");

        // module finder locates 'a' and the modules in the directory
        ModuleFinder finder
            = ModuleFinder.compose(ModuleUtils.finderOf(descriptor),
                                   ModuleFinder.of(dir));

        Configuration parent = ModuleLayer.boot().configuration();
        resolve(parent, finder, "a");
    }

    /**
     * Basic test of layer containing automatic modules
     */
    public void testInLayer() throws IOException {
        ModuleDescriptor descriptor
            = ModuleDescriptor.newModule("a")
                .requires("b")
                .requires("c")
                .build();

        // b and c are simple JAR files
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        createDummyJarFile(dir.resolve("b.jar"), "p/T.class");
        createDummyJarFile(dir.resolve("c.jar"), "q/T2.class");

        // module finder locates a and the modules in the directory
        ModuleFinder finder
            = ModuleFinder.compose(ModuleUtils.finderOf(descriptor),
                ModuleFinder.of(dir));

        Configuration parent = ModuleLayer.boot().configuration();
        Configuration cf = resolve(parent, finder, "a");
        assertTrue(cf.modules().size() == 3);

        // each module gets its own loader
        ModuleLayer layer = ModuleLayer.boot().defineModules(cf, mn -> new ClassLoader() { });

        // an unnamed module
        Module unnamed = (new ClassLoader() { }).getUnnamedModule();

        Module b = layer.findModule("b").get();
        assertTrue(b.isNamed());
        assertTrue(b.canRead(unnamed));
        testsReadsAll(b, layer);

        Module c = layer.findModule("c").get();
        assertTrue(c.isNamed());
        assertTrue(b.canRead(unnamed));
        testsReadsAll(c, layer);
    }

    /**
     * Test miscellaneous methods.
     */
    public void testMisc() throws IOException {
        Path dir = Files.createTempDirectory(USER_DIR, "mods");
        Path m_jar = createDummyJarFile(dir.resolve("m.jar"), "p/T.class");

        ModuleFinder finder = ModuleFinder.of(m_jar);

        assertTrue(finder.find("m").isPresent());
        ModuleDescriptor m = finder.find("m").get().descriptor();

        // test miscellaneous methods
        assertTrue(m.isAutomatic());
        assertFalse(m.modifiers().contains(ModuleDescriptor.Modifier.SYNTHETIC));
    }

    /**
     * Invokes parent.resolve to resolve the given root modules.
     */
    static Configuration resolve(Configuration parent,
                                 ModuleFinder finder,
                                 String... roots) {
        return parent.resolve(finder, ModuleFinder.of(), Set.of(roots));
    }

    /**
     * Finds a module in the given configuration or its parents, returning
     * the module descriptor (or null if not found)
     */
    static ModuleDescriptor findDescriptor(Configuration cf, String name) {
        Optional<ResolvedModule> om = cf.findModule(name);
        if (om.isPresent()) {
            return om.get().reference().descriptor();
        } else {
            return null;
        }
    }

    /**
     * Test that a module in a configuration reads all modules in the boot
     * configuration.
     */
    static void testReadAllBootModules(Configuration cf, String mn) {

        Set<String> bootModules = ModuleLayer.boot().modules().stream()
                .map(Module::getName)
                .collect(Collectors.toSet());

        bootModules.forEach(other -> assertTrue(reads(cf, mn, other)));

    }

    /**
     * Test that the given Module reads all module in the given layer
     * and its parent layers.
     */
    static void testsReadsAll(Module m, ModuleLayer layer) {
        // check that m reads all modules in the layer
        layer.configuration().modules().stream()
            .map(ResolvedModule::name)
            .map(layer::findModule)
            .map(Optional::get)
            .forEach(other -> assertTrue(m.canRead(other)));

        // also check parent layers
        layer.parents().forEach(l -> testsReadsAll(m, l));
    }

    /**
     * Returns {@code true} if the configuration contains module mn1
     * that reads module mn2.
     */
    static boolean reads(Configuration cf, String mn1, String mn2) {
        Optional<ResolvedModule> om = cf.findModule(mn1);
        if (!om.isPresent())
            return false;

        return om.get().reads().stream()
                .map(ResolvedModule::name)
                .anyMatch(mn2::equals);
    }

    /**
     * Creates a JAR file, optionally with a manifest, and with the given
     * entries. The entries will be empty in the resulting JAR file.
     */
    static Path createDummyJarFile(Path jarfile, Manifest man, String... entries)
        throws IOException
    {
        Path dir = Files.createTempDirectory(USER_DIR, "tmp");

        for (String entry : entries) {
            Path file = dir.resolve(entry);
            Path parent = file.getParent();
            if (parent != null)
                Files.createDirectories(parent);
            Files.createFile(file);
        }

        Path[] paths = Stream.of(entries).map(Paths::get).toArray(Path[]::new);
        JarUtils.createJarFile(jarfile, man, dir, paths);
        return jarfile;
    }

    /**
     * Creates a JAR file and with the given entries. The entries will be empty
     * in the resulting JAR file.
     */
    static Path createDummyJarFile(Path jarfile, String... entries)
        throws IOException
    {
        return createDummyJarFile(jarfile, null, entries);
    }

}