test/jdk/tools/jmod/JmodTest.java
author jlahoda
Wed, 06 Jun 2018 15:36:29 +0200
changeset 50428 8c88df2e8a78
parent 48699 f4e628259d1b
permissions -rw-r--r--
8203891: Upgrade JOpt Simple to 5.0.4 Reviewed-by: alanb, chegar, mchung

/**
 * 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
 * @bug 8142968 8166568 8166286 8170618 8168149
 * @summary Basic test for jmod
 * @library /test/lib
 * @modules jdk.compiler
 *          jdk.jlink
 * @build jdk.test.lib.compiler.CompilerUtils
 *        jdk.test.lib.util.FileUtils
 *        jdk.test.lib.Platform
 * @run testng/othervm -Djava.io.tmpdir=. JmodTest
 */

import java.io.*;
import java.lang.module.ModuleDescriptor;
import java.lang.reflect.Method;
import java.nio.file.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import jdk.test.lib.compiler.CompilerUtils;
import jdk.test.lib.util.FileUtils;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import static java.io.File.pathSeparator;
import static java.lang.module.ModuleDescriptor.Version;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.testng.Assert.*;

public class JmodTest {

    static final ToolProvider JMOD_TOOL = ToolProvider.findFirst("jmod")
        .orElseThrow(() ->
            new RuntimeException("jmod tool not found")
        );

    static final String TEST_SRC = System.getProperty("test.src", ".");
    static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
    static final Path EXPLODED_DIR = Paths.get("build");
    static final Path MODS_DIR = Paths.get("jmods");

    static final String CLASSES_PREFIX = "classes/";
    static final String CMDS_PREFIX = "bin/";
    static final String LIBS_PREFIX = "lib/";
    static final String CONFIGS_PREFIX = "conf/";

    @BeforeTest
    public void buildExplodedModules() throws IOException {
        if (Files.exists(EXPLODED_DIR))
            FileUtils.deleteFileTreeWithRetry(EXPLODED_DIR);

        for (String name : new String[] { "foo"/*, "bar", "baz"*/ } ) {
            Path dir = EXPLODED_DIR.resolve(name);
            assertTrue(compileModule(name, dir.resolve("classes")));
            copyResource(SRC_DIR.resolve("foo"),
                         dir.resolve("classes"),
                         "jdk/test/foo/resources/foo.properties");
            createCmds(dir.resolve("bin"));
            createLibs(dir.resolve("lib"));
            createConfigs(dir.resolve("conf"));
        }

        if (Files.exists(MODS_DIR))
            FileUtils.deleteFileTreeWithRetry(MODS_DIR);
        Files.createDirectories(MODS_DIR);
    }

    // JDK-8166286 - jmod fails on symlink to directory
    @Test
    public void testSymlinks() throws IOException {
        Path apaDir = EXPLODED_DIR.resolve("apa");
        Path classesDir = EXPLODED_DIR.resolve("apa").resolve("classes");
        assertTrue(compileModule("apa", classesDir));
        Path libDir = apaDir.resolve("lib");
        createFiles(libDir, List.of("foo/bar/libfoo.so"));
        try {
            Path link = Files.createSymbolicLink(
                libDir.resolve("baz"), libDir.resolve("foo").toAbsolutePath());
            assertTrue(Files.exists(link));
        } catch (IOException|UnsupportedOperationException uoe) {
            // OS does not support symlinks. Nothing to test!
            return;
        }

        Path jmod = MODS_DIR.resolve("apa.jmod");
        jmod("create",
             "--libs=" + libDir.toString(),
             "--class-path", classesDir.toString(),
             jmod.toString())
            .assertSuccess();
    }

    // JDK-8170618 - jmod should validate if any exported or open package is missing
    @Test
    public void testMissingPackages() throws IOException {
        Path apaDir = EXPLODED_DIR.resolve("apa");
        Path classesDir = EXPLODED_DIR.resolve("apa").resolve("classes");
        if (Files.exists(classesDir))
            FileUtils.deleteFileTreeWithRetry(classesDir);
        assertTrue(compileModule("apa", classesDir));
        FileUtils.deleteFileTreeWithRetry(classesDir.resolve("jdk"));
        Path jmod = MODS_DIR.resolve("apa.jmod");
        jmod("create",
             "--class-path", classesDir.toString(),
             jmod.toString())
            .assertFailure()
            .resultChecker(r -> {
                assertContains(r.output, "Packages that are exported or open in apa are not present: [jdk.test.apa]");
            });
        if (Files.exists(classesDir))
            FileUtils.deleteFileTreeWithRetry(classesDir);
    }

    @Test
    public void testList() throws IOException {
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
        jmod("create",
             "--class-path", cp,
             MODS_DIR.resolve("foo.jmod").toString())
            .assertSuccess();

        jmod("list",
             MODS_DIR.resolve("foo.jmod").toString())
            .assertSuccess()
            .resultChecker(r -> {
                // asserts dependent on the exact contents of foo
                assertContains(r.output, CLASSES_PREFIX + "module-info.class");
                assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/Foo.class");
                assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
                assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/resources/foo.properties");
            });
    }

    @Test
    public void testExtractCWD() throws IOException {
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        jmod("create",
             "--class-path", cp.toString(),
             MODS_DIR.resolve("fooExtractCWD.jmod").toString())
            .assertSuccess();

        jmod("extract",
             MODS_DIR.resolve("fooExtractCWD.jmod").toString())
            .assertSuccess()
            .resultChecker(r -> {
                // module-info should exist, but jmod will have added its Packages attr.
                assertTrue(Files.exists(Paths.get("classes/module-info.class")));
                assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
                                  Paths.get("classes/jdk/test/foo/Foo.class"));
                assertSameContent(cp.resolve("jdk/test/foo/internal/Message.class"),
                                  Paths.get("classes/jdk/test/foo/internal/Message.class"));
                assertSameContent(cp.resolve("jdk/test/foo/resources/foo.properties"),
                                  Paths.get("classes/jdk/test/foo/resources/foo.properties"));
            });
    }

    @Test
    public void testExtractDir() throws IOException {
        if (Files.exists(Paths.get("extractTestDir")))
            FileUtils.deleteFileTreeWithRetry(Paths.get("extractTestDir"));
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
        Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");

        jmod("create",
             "--conf", cf.toString(),
             "--cmds", bp.toString(),
             "--libs", lp.toString(),
             "--class-path", cp.toString(),
             MODS_DIR.resolve("fooExtractDir.jmod").toString())
            .assertSuccess();

        jmod("extract",
             "--dir", "extractTestDir",
             MODS_DIR.resolve("fooExtractDir.jmod").toString())
            .assertSuccess();

        jmod("extract",
             "--dir", "extractTestDir",
             MODS_DIR.resolve("fooExtractDir.jmod").toString())
            .assertSuccess()
            .resultChecker(r -> {
                // check a sample of the extracted files
                Path p = Paths.get("extractTestDir");
                assertTrue(Files.exists(p.resolve("classes/module-info.class")));
                assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
                                  p.resolve("classes/jdk/test/foo/Foo.class"));
                assertSameContent(bp.resolve("first"),
                                  p.resolve(CMDS_PREFIX).resolve("first"));
                assertSameContent(lp.resolve("first.so"),
                                  p.resolve(LIBS_PREFIX).resolve("second.so"));
                assertSameContent(cf.resolve("second.cfg"),
                                  p.resolve(CONFIGS_PREFIX).resolve("second.cfg"));
            });
    }

    @Test
    public void testMainClass() throws IOException {
        Path jmod = MODS_DIR.resolve("fooMainClass.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();

        jmod("create",
             "--class-path", cp,
             "--main-class", "jdk.test.foo.Foo",
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                Optional<String> omc = getModuleDescriptor(jmod).mainClass();
                assertTrue(omc.isPresent());
                assertEquals(omc.get(), "jdk.test.foo.Foo");
            });
    }

    @Test
    public void testModuleVersion() throws IOException {
        Path jmod = MODS_DIR.resolve("fooVersion.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();

        jmod("create",
             "--class-path", cp,
             "--module-version", "5.4.3",
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                Optional<Version> ov = getModuleDescriptor(jmod).version();
                assertTrue(ov.isPresent());
                assertEquals(ov.get().toString(), "5.4.3");
            });
    }

    @Test
    public void testConfig() throws IOException {
        Path jmod = MODS_DIR.resolve("fooConfig.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");

        jmod("create",
             "--class-path", cp.toString(),
             "--config", cf.toString(),
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                try (Stream<String> s1 = findFiles(cf).map(p -> CONFIGS_PREFIX + p);
                     Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
                    Set<String> expectedFilenames = Stream.concat(s1, s2)
                                                          .collect(toSet());
                    assertJmodContent(jmod, expectedFilenames);
                }
            });
    }

    @Test
    public void testCmds() throws IOException {
        Path jmod = MODS_DIR.resolve("fooCmds.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");

        jmod("create",
             "--cmds", bp.toString(),
             "--class-path", cp.toString(),
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                try (Stream<String> s1 = findFiles(bp).map(p -> CMDS_PREFIX + p);
                     Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
                    Set<String> expectedFilenames = Stream.concat(s1,s2)
                                                          .collect(toSet());
                    assertJmodContent(jmod, expectedFilenames);
                }
            });
    }

    @Test
    public void testLibs() throws IOException {
        Path jmod = MODS_DIR.resolve("fooLibs.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");

        jmod("create",
             "--libs=" + lp.toString(),
             "--class-path", cp.toString(),
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
                     Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
                    Set<String> expectedFilenames = Stream.concat(s1,s2)
                                                          .collect(toSet());
                    assertJmodContent(jmod, expectedFilenames);
                }
            });
    }

    @Test
    public void testAll() throws IOException {
        Path jmod = MODS_DIR.resolve("fooAll.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
        Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");

        jmod("create",
             "--conf", cf.toString(),
             "--cmds=" + bp.toString(),
             "--libs=" + lp.toString(),
             "--class-path", cp.toString(),
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
                     Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p);
                     Stream<String> s3 = findFiles(bp).map(p -> CMDS_PREFIX + p);
                     Stream<String> s4 = findFiles(cf).map(p -> CONFIGS_PREFIX + p)) {
                    Set<String> expectedFilenames = Stream.concat(Stream.concat(s1,s2),
                                                                  Stream.concat(s3, s4))
                                                          .collect(toSet());
                    assertJmodContent(jmod, expectedFilenames);
                }
            });
    }

    @Test
    public void testExcludes() throws IOException {
        Path jmod = MODS_DIR.resolve("fooLibs.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");

        jmod("create",
             "--libs=" + lp.toString(),
             "--class-path", cp.toString(),
             "--exclude", "**internal**",
             "--exclude", "first.so",
             jmod.toString())
             .assertSuccess()
             .resultChecker(r -> {
                 Set<String> expectedFilenames = new HashSet<>();
                 expectedFilenames.add(CLASSES_PREFIX + "module-info.class");
                 expectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/Foo.class");
                 expectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/resources/foo.properties");
                 expectedFilenames.add(LIBS_PREFIX + "second.so");
                 expectedFilenames.add(LIBS_PREFIX + "third/third.so");
                 assertJmodContent(jmod, expectedFilenames);

                 Set<String> unexpectedFilenames = new HashSet<>();
                 unexpectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
                 unexpectedFilenames.add(LIBS_PREFIX + "first.so");
                 assertJmodDoesNotContain(jmod, unexpectedFilenames);
             });
    }

    @Test
    public void describe() throws IOException {
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
        jmod("create",
             "--class-path", cp,
              MODS_DIR.resolve("describeFoo.jmod").toString())
             .assertSuccess();

        jmod("describe",
             MODS_DIR.resolve("describeFoo.jmod").toString())
             .assertSuccess()
             .resultChecker(r -> {
                 // Expect similar output: "foo... exports jdk.test.foo ...
                 //   ... requires java.base mandated... contains jdk.test.foo.internal"
                 Pattern p = Pattern.compile("foo\\s+exports\\s+jdk.test.foo");
                 assertTrue(p.matcher(r.output).find(),
                           "Expecting to find \"foo... exports jdk.test.foo\"" +
                                "in output, but did not: [" + r.output + "]");
                 p = Pattern.compile(
                        "requires\\s+java.base\\s+mandated\\s+contains\\s+jdk.test.foo.internal");
                 assertTrue(p.matcher(r.output).find(),
                           "Expecting to find \"requires java.base mandated..., " +
                                "contains jdk.test.foo.internal ...\"" +
                                "in output, but did not: [" + r.output + "]");
             });
    }

    @Test
    public void testDuplicateEntries() throws IOException {
        Path jmod = MODS_DIR.resolve("testDuplicates.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");

        jmod("create",
             "--class-path", cp + pathSeparator + cp,
             jmod.toString())
             .assertSuccess()
             .resultChecker(r ->
                 assertContains(r.output, "Warning: ignoring duplicate entry")
             );

        FileUtils.deleteFileIfExistsWithRetry(jmod);
        jmod("create",
             "--class-path", cp,
             "--libs", lp.toString() + pathSeparator + lp.toString(),
             jmod.toString())
             .assertSuccess()
             .resultChecker(r ->
                 assertContains(r.output, "Warning: ignoring duplicate entry")
             );
    }

    @Test
    public void testIgnoreModuleInfoInOtherSections() throws IOException {
        Path jmod = MODS_DIR.resolve("testIgnoreModuleInfoInOtherSections.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();

        jmod("create",
            "--class-path", cp,
            "--libs", cp,
            jmod.toString())
            .assertSuccess()
            .resultChecker(r ->
                assertContains(r.output, "Warning: ignoring entry")
            );

        FileUtils.deleteFileIfExistsWithRetry(jmod);
        jmod("create",
             "--class-path", cp,
             "--cmds", cp,
             jmod.toString())
             .assertSuccess()
             .resultChecker(r ->
                 assertContains(r.output, "Warning: ignoring entry")
             );
    }

    @Test
    public void testLastOneWins() throws IOException {
        Path workDir = Paths.get("lastOneWins");
        if (Files.exists(workDir))
            FileUtils.deleteFileTreeWithRetry(workDir);
        Files.createDirectory(workDir);
        Path jmod = MODS_DIR.resolve("lastOneWins.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
        Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
        Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
        Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");

        Path shouldNotBeAdded = workDir.resolve("shouldNotBeAdded");
        Files.createDirectory(shouldNotBeAdded);
        Files.write(shouldNotBeAdded.resolve("aFile"), "hello".getBytes(UTF_8));

        // Pairs of options. For options with required arguments the last one
        // should win ( first should be effectively ignored, but may still be
        // validated ).
        jmod("create",
             "--conf", shouldNotBeAdded.toString(),
             "--conf", cf.toString(),
             "--cmds", shouldNotBeAdded.toString(),
             "--cmds", bp.toString(),
             "--libs", shouldNotBeAdded.toString(),
             "--libs", lp.toString(),
             "--class-path", shouldNotBeAdded.toString(),
             "--class-path", cp.toString(),
             "--main-class", "does.NotExist",
             "--main-class", "jdk.test.foo.Foo",
             "--module-version", "00001",
             "--module-version", "5.4.3",
             "--do-not-resolve-by-default",
             "--do-not-resolve-by-default",
             "--warn-if-resolved=incubating",
             "--warn-if-resolved=deprecated",
             MODS_DIR.resolve("lastOneWins.jmod").toString())
            .assertSuccess()
            .resultChecker(r -> {
                ModuleDescriptor md = getModuleDescriptor(jmod);
                Optional<String> omc = md.mainClass();
                assertTrue(omc.isPresent());
                assertEquals(omc.get(), "jdk.test.foo.Foo");
                Optional<Version> ov = md.version();
                assertTrue(ov.isPresent());
                assertEquals(ov.get().toString(), "5.4.3");

                try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
                     Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p);
                     Stream<String> s3 = findFiles(bp).map(p -> CMDS_PREFIX + p);
                     Stream<String> s4 = findFiles(cf).map(p -> CONFIGS_PREFIX + p)) {
                    Set<String> expectedFilenames = Stream.concat(Stream.concat(s1,s2),
                                                                  Stream.concat(s3, s4))
                                                          .collect(toSet());
                    assertJmodContent(jmod, expectedFilenames);
                }
            });

        jmod("extract",
             "--dir", "blah",
             "--dir", "lastOneWinsExtractDir",
             jmod.toString())
            .assertSuccess()
            .resultChecker(r -> {
                assertTrue(Files.exists(Paths.get("lastOneWinsExtractDir")));
                assertTrue(Files.notExists(Paths.get("blah")));
            });
    }

    @Test
    public void testPackagesAttribute() throws IOException {
        Path jmod = MODS_DIR.resolve("foo.jmod");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();

        Set<String> expectedPackages = Set.of("jdk.test.foo",
                                              "jdk.test.foo.internal",
                                              "jdk.test.foo.resources");

        jmod("create",
             "--class-path", cp,
             jmod.toString())
             .assertSuccess()
             .resultChecker(r -> {
                 Set<String> pkgs = getModuleDescriptor(jmod).packages();
                 assertEquals(pkgs, expectedPackages);
             });
        }

    @Test
    public void testVersion() {
        jmod("--version")
            .assertSuccess()
            .resultChecker(r -> {
                assertContains(r.output, System.getProperty("java.version"));
            });
    }

    @Test
    public void testHelp() {
        jmod("--help")
            .assertSuccess()
            .resultChecker(r -> {
                assertTrue(r.output.startsWith("Usage: jmod"), "Help not printed");
                assertFalse(r.output.contains("--do-not-resolve-by-default"));
                assertFalse(r.output.contains("--warn-if-resolved"));
            });
    }

    @Test
    public void testHelpExtra() {
        jmod("--help-extra")
            .assertSuccess()
            .resultChecker(r -> {
                assertTrue(r.output.startsWith("Usage: jmod"), "Extra help not printed");
                assertContains(r.output, "--do-not-resolve-by-default");
                assertContains(r.output, "--warn-if-resolved");
            });
    }

    @Test
    public void testTmpFileRemoved() throws IOException {
        // Implementation detail: jmod tool creates <jmod-file>.tmp
        // Ensure that it is removed in the event of a failure.
        // The failure in this case is a class in the unnamed package.

        Path jmod = MODS_DIR.resolve("testTmpFileRemoved.jmod");
        Path tmp = MODS_DIR.resolve(".testTmpFileRemoved.jmod.tmp");
        FileUtils.deleteFileIfExistsWithRetry(jmod);
        FileUtils.deleteFileIfExistsWithRetry(tmp);
        String cp = EXPLODED_DIR.resolve("foo").resolve("classes") + File.pathSeparator +
                    EXPLODED_DIR.resolve("foo").resolve("classes")
                                .resolve("jdk").resolve("test").resolve("foo").toString();

        jmod("create",
             "--class-path", cp,
             jmod.toString())
            .assertFailure()
            .resultChecker(r -> {
                assertContains(r.output, "unnamed package");
                assertTrue(Files.notExists(tmp), "Unexpected tmp file:" + tmp);
            });
    }

    // ---

    static boolean compileModule(String name, Path dest) throws IOException {
        return CompilerUtils.compile(SRC_DIR.resolve(name), dest);
    }

    static void assertContains(String output, String subString) {
        if (output.contains(subString))
            assertTrue(true);
        else
            assertTrue(false,"Expected to find [" + subString + "], in output ["
                           + output + "]" + "\n");
    }

    static ModuleDescriptor getModuleDescriptor(Path jmod) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        try (FileSystem fs = FileSystems.newFileSystem(jmod, cl)) {
            String p = "/classes/module-info.class";
            try (InputStream is = Files.newInputStream(fs.getPath(p))) {
                return ModuleDescriptor.read(is);
            }
        } catch (IOException ioe) {
            throw new UncheckedIOException(ioe);
        }
    }

    static Stream<String> findFiles(Path dir) {
        try {
            return Files.find(dir, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())
                        .map(dir::relativize)
                        .map(Path::toString)
                        .map(p -> p.replace(File.separator, "/"));
        } catch (IOException x) {
            throw new UncheckedIOException(x);
        }
    }

    static Set<String> getJmodContent(Path jmod) {
        JmodResult r = jmod("list", jmod.toString()).assertSuccess();
        return Stream.of(r.output.split("\r?\n")).collect(toSet());
    }

    static void assertJmodContent(Path jmod, Set<String> expected) {
        Set<String> actual = getJmodContent(jmod);
        if (!Objects.equals(actual, expected)) {
            Set<String> unexpected = new HashSet<>(actual);
            unexpected.removeAll(expected);
            Set<String> notFound = new HashSet<>(expected);
            notFound.removeAll(actual);
            StringBuilder sb = new StringBuilder();
            sb.append("Unexpected but found:\n");
            unexpected.forEach(s -> sb.append("\t" + s + "\n"));
            sb.append("Expected but not found:\n");
            notFound.forEach(s -> sb.append("\t" + s + "\n"));
            assertTrue(false, "Jmod content check failed.\n" + sb.toString());
        }
    }

    static void assertJmodDoesNotContain(Path jmod, Set<String> unexpectedNames) {
        Set<String> actual = getJmodContent(jmod);
        Set<String> unexpected = new HashSet<>();
        for (String name : unexpectedNames) {
            if (actual.contains(name))
                unexpected.add(name);
        }
        if (!unexpected.isEmpty()) {
            StringBuilder sb = new StringBuilder();
            for (String s : unexpected)
                sb.append("Unexpected but found: " + s + "\n");
            sb.append("In :");
            for (String s : actual)
                sb.append("\t" + s + "\n");
            assertTrue(false, "Jmod content check failed.\n" + sb.toString());
        }
    }

    static void assertSameContent(Path p1, Path p2) {
        try {
            byte[] ba1 = Files.readAllBytes(p1);
            byte[] ba2 = Files.readAllBytes(p2);
            assertEquals(ba1, ba2);
        } catch (IOException x) {
            throw new UncheckedIOException(x);
        }
    }

    static JmodResult jmod(String... args) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(baos);
        System.out.println("jmod " + Arrays.asList(args));
        int ec = JMOD_TOOL.run(ps, ps, args);
        return new JmodResult(ec, new String(baos.toByteArray(), UTF_8));
    }

    static class JmodResult {
        final int exitCode;
        final String output;

        JmodResult(int exitValue, String output) {
            this.exitCode = exitValue;
            this.output = output;
        }
        JmodResult assertSuccess() { assertTrue(exitCode == 0, output); return this; }
        JmodResult assertFailure() { assertTrue(exitCode != 0, output); return this; }
        JmodResult resultChecker(Consumer<JmodResult> r) { r.accept(this); return this; }
    }

    static void createCmds(Path dir) throws IOException {
        List<String> files = Arrays.asList(
                "first", "second", "third" + File.separator + "third");
        createFiles(dir, files);
    }

    static void createLibs(Path dir) throws IOException {
        List<String> files = Arrays.asList(
                "first.so", "second.so", "third" + File.separator + "third.so");
        createFiles(dir, files);
    }

    static void createConfigs(Path dir) throws IOException {
        List<String> files = Arrays.asList(
                "first.cfg", "second.cfg", "third" + File.separator + "third.cfg");
        createFiles(dir, files);
    }

    static void createFiles(Path dir, List<String> filenames) throws IOException {
        for (String name : filenames) {
            Path file = dir.resolve(name);
            Files.createDirectories(file.getParent());
            Files.createFile(file);
            try (OutputStream os  = Files.newOutputStream(file)) {
                os.write("blahblahblah".getBytes(UTF_8));
            }
        }
    }

    static void copyResource(Path srcDir, Path dir, String resource) throws IOException {
        Path dest = dir.resolve(resource);
        Files.deleteIfExists(dest);

        Files.createDirectories(dest.getParent());
        Files.copy(srcDir.resolve(resource), dest);
    }

    // Standalone entry point.
    public static void main(String[] args) throws Throwable {
        JmodTest test = new JmodTest();
        test.buildExplodedModules();
        for (Method m : JmodTest.class.getDeclaredMethods()) {
            if (m.getAnnotation(Test.class) != null) {
                System.out.println("Invoking " + m.getName());
                m.invoke(test);
            }
        }
    }
}