package org.graalvm.component.installer.commands;
import java.io.File;
import org.graalvm.component.installer.CommandTestBase;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.graalvm.component.installer.remote.CatalogIterable;
import org.graalvm.component.installer.Commands;
import org.graalvm.component.installer.CommonConstants;
import org.graalvm.component.installer.ComponentIterable;
import org.graalvm.component.installer.DependencyException;
import org.graalvm.component.installer.FailedOperationException;
import org.graalvm.component.installer.SystemUtils;
import org.graalvm.component.installer.model.CatalogContents;
import org.graalvm.component.installer.model.ComponentInfo;
import org.graalvm.component.installer.persist.ProxyResource;
import org.graalvm.component.installer.remote.RemoteCatalogDownloader;
import org.graalvm.component.installer.persist.test.Handler;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
public class InstallTest extends CommandTestBase {
@Rule public final ProxyResource proxyResource = new ProxyResource();
InstallCommand inst;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
files.add(dataFile("truffleruby2.jar").toFile());
}
@Test
public void testHelp() throws Exception {
class F extends FeedbackAdapter {
List<String> outputKeys = new ArrayList<>();
@Override
public void output(String bundleKey, Object... params) {
outputKeys.add(bundleKey);
}
}
F fe = new F();
delegateFeedback(fe);
options.put(Commands.OPTION_HELP, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
assertFalse(Files.list(targetPath).findFirst().isPresent());
assertTrue(fe.outputKeys.get(0).toLowerCase().contains("help"));
}
@Test
public void testDryRunInstall() throws IOException {
options.put(Commands.OPTION_DRY_RUN, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
assertFalse(Files.list(targetPath).findFirst().isPresent());
}
@Test
public void testValidateInstallFail() throws IOException {
options.put(Commands.OPTION_VALIDATE, "");
files.add(dataFile("truffleruby-i386.zip").toFile());
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
try {
inst.execute();
fail("The second component should fail dependencies");
} catch (DependencyException ex) {
}
assertFalse(Files.list(targetPath).findFirst().isPresent());
}
private String errorKey;
private String msg;
@Test
public void testIgnoreFailedInstall() throws IOException {
options.put(Commands.OPTION_VALIDATE, "");
files.add(dataFile("truffleruby-i386.zip").toFile());
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.setIgnoreFailures(true);
delegateFeedback(new FeedbackAdapter() {
@Override
public void error(String key, Throwable t, Object... params) {
errorKey = key;
msg = t.getMessage();
}
});
inst.execute();
assertFalse(Files.list(targetPath).findFirst().isPresent());
assertEquals("VERIFY_Dependency_Failed", msg);
assertEquals("INSTALL_IgnoreFailedInstallation2", errorKey);
}
@Test
public void testDryRunForce() throws IOException {
options.put(Commands.OPTION_DRY_RUN, "");
options.put(Commands.OPTION_FORCE, "");
files.add(dataFile("truffleruby-i386.zip").toFile());
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
assertFalse(Files.list(targetPath).findFirst().isPresent());
}
@Test
public void testValidateInstall() throws IOException {
options.put(Commands.OPTION_VALIDATE, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
assertFalse(Files.list(targetPath).findFirst().isPresent());
}
@Test
public void testFailOnExistingComponent() throws IOException {
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
options.put(Commands.OPTION_FAIL_EXISTING, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
files.add(dataFile("truffleruby3.jar").toFile());
exception.expect(DependencyException.class);
exception.expectMessage("VERIFY_ComponentExists");
inst.execute();
}
@Test
public void testSkipExistingComponent() throws IOException {
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
File f = new File(folder.getRoot(), "inst");
File binRuby = SystemUtils.resolveRelative(f.toPath(), "jre/bin/ruby").toFile();
assertTrue("Ruby must be installed", binRuby.exists());
Files.walk(f.toPath()).forEach((p) -> {
try {
if (!p.equals(f.toPath()) && Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) {
Files.delete(p);
}
} catch (IOException ex) {
Logger.getLogger(InstallTest.class.getName()).log(Level.SEVERE, null, ex);
}
});
assertFalse("Ruby must be deleted", binRuby.exists());
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
files.add(dataFile("truffleruby3.jar").toFile());
inst.execute();
assertFalse("Component must not be processed", binRuby.exists());
}
ComponentIterable componentIterable;
@Override
public ComponentIterable existingFiles() throws FailedOperationException {
if (componentIterable != null) {
return componentIterable;
}
return super.existingFiles();
}
@Test
public void testFailOnExistingFromCatalog() throws Exception {
ComponentInfo fakeInfo = new ComponentInfo("ruby", "Fake ruby", "1.0");
storage.installed.add(fakeInfo);
URL u = new URL("test://graalvm.io/download/catalog");
URL u2 = new URL(u, "graalvm-ruby.zip");
Handler.bind(u.toString(), getClass().getResource("catalog"));
storage.graalInfo.put(CommonConstants.CAP_GRAALVM_VERSION, "0.33-dev");
initCatalogIterable(u);
textParams.add("ruby");
options.put(Commands.OPTION_FAIL_EXISTING, "");
files.clear();
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
try {
inst.execute();
} catch (DependencyException.Conflict ex) {
assertEquals("VERIFY_ComponentExists", ex.getMessage());
}
assertFalse(Handler.isVisited(u2));
}
private void initCatalogIterable(URL u) {
RemoteCatalogDownloader rcd = new RemoteCatalogDownloader(
this,
this,
u);
registry = new CatalogContents(this, rcd.getStorage(), localRegistry);
componentIterable = new CatalogIterable(this, this);
}
@Test
public void testSkipExistingFromCatalog() throws Exception {
ComponentInfo fakeInfo = new ComponentInfo("ruby", "Fake ruby", "1.0");
storage.installed.add(fakeInfo);
URL u = new URL("test://graalvm.io/download/catalog");
URL u2 = new URL(u, "graalvm-ruby.zip");
Handler.bind(u.toString(), getClass().getResource("catalog"));
Handler.bind(u2.toString(), getClass().getResource("graalvm-ruby.zip"));
storage.graalInfo.put(CommonConstants.CAP_GRAALVM_VERSION, "0.33-dev");
initCatalogIterable(u);
textParams.add("ruby");
files.clear();
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
try {
inst.execute();
} catch (DependencyException.Conflict ex) {
fail("Should not raise an error");
}
assertFalse("Should not touch the remote file", Handler.isVisited(u2));
}
@Test
public void testReplaceExistingComponent() throws IOException {
options.put(Commands.OPTION_REPLACE_COMPONENTS, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
files.add(dataFile("truffleruby3.jar").toFile());
inst.execute();
}
@Test
public void testFailInstallCleanup() throws IOException {
Path offending = targetPath.resolve(SystemUtils.fromCommonString("jre/bin/ruby"));
Files.createDirectories(offending.getParent());
Files.createFile(offending);
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
try {
inst.execute();
fail("Exception expected");
} catch (IOException | FailedOperationException ex) {
}
Files.delete(offending);
Files.delete(offending.getParent());
Files.delete(offending.getParent().getParent());
assertFalse(Files.list(targetPath).findFirst().isPresent());
}
@Test
public void testPostinstMessagePrinted() throws Exception {
AtomicBoolean printed = new AtomicBoolean();
delegateFeedback(new FeedbackAdapter() {
@Override
public boolean verbatimOut(String aMsg, boolean beVerbose) {
if ("Postinst".equals(aMsg)) {
printed.set(true);
}
return super.verbatimOut(aMsg, beVerbose);
}
});
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
assertTrue("Postinst message must be printed", printed.get());
}
private static final String GOLDEN_MESSAGE = "\n" +
"IMPORTANT NOTE:\n" +
"---------------\n" +
"The Ruby openssl C extension needs to be recompiled on your system to work with the installed libssl.\n" +
"Make sure headers for libssl are installed, see https://github.com/oracle/truffleruby/blob/master/doc/user/installing-libssl.md for details.\n" +
"Then run the following command:\n" +
" ${graalvm_home}/jre/languages/ruby/lib/truffle/post_install_hook.sh\n";
@Test
public void testPostinstMessageFormat() throws Exception {
String[] formatted = new String[1];
files.set(0, dataFile("postinst.jar").toFile());
delegateFeedback(new FeedbackAdapter() {
@Override
public boolean verbatimOut(String aMsg, boolean beVerbose) {
if (aMsg.contains("Ruby openssl")) {
formatted[0] = aMsg;
}
return super.verbatimOut(aMsg, beVerbose);
}
});
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
assertNotNull("Postinst message must be printed", formatted[0]);
String check = GOLDEN_MESSAGE.replace("${graalvm_home}", getGraalHomePath().toString());
assertEquals(check, formatted[0]);
}
@Test
public void testInstallMissingComponent() throws Exception {
ComponentInfo fakeInfo = new ComponentInfo("ruby", "Fake ruby", "1.0");
storage.installed.add(fakeInfo);
}
@Test
public void testRefuseNonAdminInstall() throws Exception {
options.put(Commands.OPTION_DRY_RUN, "");
storage.writableUser = "hero";
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
exception.expect(FailedOperationException.class);
exception.expectMessage("ADMIN");
inst.execute();
}
private static final String BLOCKED_CONTENT = "This is a blocked file";
private static final String INSTALL_CONTENT = "#!/usr/bin/env bash";
@Test
public void testReplaceExistingComponentWithLockedFiles() throws IOException {
options.put(Commands.OPTION_REPLACE_COMPONENTS, "");
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
inst.execute();
Path blockedFile = targetPath.resolve(SystemUtils.fromCommonString("jre/languages/ruby/bin/rake"));
Path copyDir = targetPath.resolve(SystemUtils.fromCommonString("jre/languages/ruby/bin.new"));
Path copyFile = copyDir.resolve("rake");
Files.write(blockedFile, Collections.singletonList(BLOCKED_CONTENT));
BlockedFileOps blockedOps = new BlockedFileOps();
fileOps = blockedOps;
fileOps.init(this);
fileOps.setRootPath(targetPath);
blockedOps.blockedPaths.add(blockedFile);
Path delayDeletes = folder.newFile("delayDeletes").toPath();
Path copiedFiles = folder.newFile("copiedDirs").toPath();
blockedOps.setDelayDeletedList(delayDeletes);
blockedOps.setCopyContents(copiedFiles);
inst = new InstallCommand();
inst.init(this, withBundle(InstallCommand.class));
files.add(dataFile("truffleruby3.jar").toFile());
inst.execute();
assertEquals(BLOCKED_CONTENT, Files.readAllLines(blockedFile).get(0));
assertEquals(INSTALL_CONTENT, Files.readAllLines(copyFile).get(0));
}
}