/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.voltdb; import static org.junit.Assert.*; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.util.EnumSet; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.voltdb.VoltDB.Configuration; import org.voltdb.VoltDB.SimulatedExitException; import org.voltdb.catalog.Catalog; import org.voltdb.compiler.VoltCompiler; import org.voltdb.compiler.VoltProjectBuilder; import org.voltdb.utils.CatalogUtil; import org.voltdb.utils.InMemoryJarfile; import org.voltdb.utils.VoltFile; import com.google_voltpatches.common.base.Joiner; import com.google_voltpatches.common.io.CharStreams; import com.google_voltpatches.common.reflect.ClassPath; import com.google_voltpatches.common.reflect.ClassPath.ClassInfo; /** Tests starting the server with init + start without a schema, * and 'init --schema --classes'. * Starting after 'init --schema' is covered in "TestStartWithSchema" and (in Pro) "TestStartWithSchemaAndDurability". */ final public class TestInitStartAction { static File rootDH; static File cmdlogDH; private static final String[] deploymentXML = { "<?xml version=\"1.0\"?>", "<deployment>", " <cluster hostcount=\"1\"/>", " <paths>", " <voltdbroot path=\"_VOLTDBROOT_PATH_\"/>", " <commandlog path=\"_COMMANDLOG_PATH_\"/>", " </paths>", " <httpd enabled=\"true\">", " <jsonapi enabled=\"true\"/>", " </httpd>", " <commandlog enabled=\"false\"/>", "</deployment>" }; static final Pattern voltdbrootRE = Pattern.compile("_VOLTDBROOT_PATH_"); static final Pattern commandlogRE = Pattern.compile("_COMMANDLOG_PATH_"); static File legacyDeploymentFH; @ClassRule static public final TemporaryFolder tmp = new TemporaryFolder(); @BeforeClass public static void setupClass() throws Exception { rootDH = tmp.newFolder(); cmdlogDH = new File(tmp.newFolder(), "commandlog"); legacyDeploymentFH = new File(rootDH, "deployment.xml"); try (FileWriter fw = new FileWriter(legacyDeploymentFH)) { Matcher mtc = voltdbrootRE.matcher(Joiner.on('\n').join(deploymentXML)); String expnd = mtc.replaceAll(new File(rootDH, "voltdbroot").getPath()); mtc = commandlogRE.matcher(expnd); expnd = mtc.replaceAll(cmdlogDH.getPath()); fw.write(expnd); } System.setProperty("VOLT_JUSTATEST", "YESYESYES"); VoltDB.ignoreCrash = true; } AtomicReference<Throwable> serverException = new AtomicReference<>(null); final Thread.UncaughtExceptionHandler handleUncaught = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { serverException.compareAndSet(null, e); } }; /** Verifies that the VoltDB exit 'crash' was a simulated exit with the specified exit code. * @param exitCode Expected exit code from VoltDB */ private void expectSimulatedExit(int exitCode){ assertNotNull(serverException.get()); if (!(serverException.get() instanceof VoltDB.SimulatedExitException)) { System.err.println("got an unexpected exception"); serverException.get().printStackTrace(System.err); if (VoltDB.wasCrashCalled) { System.err.println("Crash message is:\n "+ VoltDB.crashMessage); } } assertTrue(serverException.get() instanceof VoltDB.SimulatedExitException); VoltDB.SimulatedExitException exitex = (VoltDB.SimulatedExitException)serverException.get(); assertEquals(exitCode, exitex.getStatus()); } /** Clears recorded crash (or simulated exit) in preparation for another test. */ private void clearCrash(){ VoltDB.wasCrashCalled = false; VoltDB.crashMessage = null; serverException.set(null); } /** Tests starting an empty database with the NewCLI commands "init" and "start", * plus a few error cases. */ @Test public void testInitStartAction() throws Exception { File deplFH = new VoltFile(new VoltFile(new VoltFile(rootDH, "voltdbroot"), "config"), "deployment.xml"); Configuration c1 = new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "deployment", legacyDeploymentFH.getPath()}); ServerThread server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); c1.m_forceVoltdbCreate = false; server.start(); server.join(); expectSimulatedExit(0); assertTrue(deplFH.exists() && deplFH.isFile() && deplFH.canRead()); if (c1.m_isEnterprise) { assertTrue(cmdlogDH.exists() && cmdlogDH.isDirectory() && cmdlogDH.canRead() && cmdlogDH.canWrite() && cmdlogDH.canExecute()); for (int i=0; i<10; ++i) { new FileOutputStream(new File(cmdlogDH, String.format("dummy-%02d.log", i))).close(); } assertEquals(10, cmdlogDH.list().length); } serverException.set(null); // server thread sets m_forceVoltdbCreate to true by default c1 = new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "force", "deployment", legacyDeploymentFH.getPath()}); assertTrue(c1.m_forceVoltdbCreate); server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); expectSimulatedExit(0); assertTrue(deplFH.exists() && deplFH.isFile() && deplFH.canRead()); if (c1.m_isEnterprise) { assertTrue(cmdlogDH.exists() && cmdlogDH.isDirectory() && cmdlogDH.canRead() && cmdlogDH.canWrite() && cmdlogDH.canExecute()); assertEquals(0, cmdlogDH.list().length); } try { c1 = new Configuration(new String[]{"initialize", "voltdbroot", rootDH.getPath()}); fail("did not detect prexisting initialization"); } catch (VoltDB.SimulatedExitException e) { assertEquals(-1, e.getStatus()); } VoltDB.wasCrashCalled = false; VoltDB.crashMessage = null; serverException.set(null); c1 = new Configuration(new String[]{"create", "deployment", legacyDeploymentFH.getPath(), "host", "localhost"}); server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); assertNotNull(serverException.get()); assertTrue(serverException.get() instanceof AssertionError); assertTrue(VoltDB.wasCrashCalled); assertTrue(VoltDB.crashMessage.contains("Cannot use legacy start action")); if (!c1.m_isEnterprise) return; clearCrash(); c1 = new Configuration(new String[]{"recover", "deployment", legacyDeploymentFH.getPath(), "host", "localhost"}); server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); assertNotNull(serverException.get()); assertTrue(serverException.get() instanceof AssertionError); assertTrue(VoltDB.wasCrashCalled); assertTrue(VoltDB.crashMessage.contains("Cannot use legacy start action")); // this test which action should be considered legacy EnumSet<StartAction> legacyOnes = EnumSet.complementOf(EnumSet.of(StartAction.INITIALIZE,StartAction.PROBE, StartAction.GET)); assertTrue(legacyOnes.stream().allMatch(StartAction::isLegacy)); } /* * "voltdb init --schema --procedures" tests: * 1. Positive test with valid schema that requires no procedures * 2a. Positive test with valid schema and procedures that are in CLASSPATH * 2b. Negative test with valid files but not "init --force" * 3. Negative test with a bad schema * 4. Negative test with procedures missing * * Note that SimulatedExitException is thrown by the command line parser with no descriptive details. * VoltDB.crashLocalVoltDB() throws an AssertionError with the message "Faux crash of VoltDB successful." */ /** Verifies that the staged catalog matches what VoltCompiler emits given the supplied schema. * @param schema Schema used to generate the staged catalog * @throws Exception upon test failure or error (unable to write temp file for example) */ private void validateStagedCatalog(String schema, InMemoryJarfile proceduresJar) throws Exception { // setup reference point for the supplied schema File schemaFile = VoltProjectBuilder.writeStringToTempFile(schema); schemaFile.deleteOnExit(); File referenceFile = File.createTempFile("reference", ".jar"); referenceFile.deleteOnExit(); VoltCompiler compiler = new VoltCompiler(false); final boolean success = compiler.compileFromDDL(referenceFile.getAbsolutePath(), schemaFile.getPath()); assertEquals(true, success); InMemoryJarfile referenceCatalogJar = new InMemoryJarfile(referenceFile); Catalog referenceCatalog = new Catalog(); referenceCatalog.execute(CatalogUtil.getSerializedCatalogStringFromJar(referenceCatalogJar)); // verify that the staged catalog is identical File stagedJarFile = new VoltFile(RealVoltDB.getStagedCatalogPath(rootDH.getPath() + File.separator + "voltdbroot")); assertEquals(true, stagedJarFile.isFile()); InMemoryJarfile stagedCatalogJar = new InMemoryJarfile(stagedJarFile); Catalog stagedCatalog = new Catalog(); stagedCatalog.execute(CatalogUtil.getSerializedCatalogStringFromJar(stagedCatalogJar)); assertEquals(true, referenceCatalog.equals(stagedCatalog)); assertEquals(true, stagedCatalog.equals(referenceCatalog)); assertEquals(true, referenceFile.delete()); assertEquals(true, schemaFile.delete()); if (proceduresJar != null) { // Validate that the list of files in the supplied jarfile are present in the staged catalog also. InMemoryJarfile strippedReferenceJar = CatalogUtil.getCatalogJarWithoutDefaultArtifacts(proceduresJar); InMemoryJarfile strippedTestJar = CatalogUtil.getCatalogJarWithoutDefaultArtifacts(stagedCatalogJar); for (Entry<String, byte[]> entry : strippedReferenceJar.entrySet()) { System.out.println("Checking " + entry.getKey()); byte[] testClass = strippedTestJar.get(entry.getKey()); assertNotNull(entry.getKey() + " was not found in staged catalog", testClass); assertArrayEquals(entry.getValue(), testClass); } } } /** Test that a valid schema with no procedures can be used to stage a matching catalog. * @throws Exception upon failure or error */ @Test public void testInitWithSchemaValidNoProcedures() throws Exception { final String schema = "create table books (cash integer default 23 not null, title varchar(3) default 'foo', PRIMARY KEY(cash));" + "create procedure Foo as select * from books;\n" + "PARTITION TABLE books ON COLUMN cash;"; File schemaFile = VoltProjectBuilder.writeStringToTempFile(schema); Configuration c1 = new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "force", "schema", schemaFile.getPath()}); ServerThread server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); expectSimulatedExit(0); validateStagedCatalog(schema, null); assertEquals(true, schemaFile.delete()); } /** Test that a valid schema with procedures can be used to stage a matching catalog, * but running a second time without 'init --force' fails due to existing artifacts. * @throws Exception upon failure or error */ @Test public void testInitWithSchemaValidWithProcedures() throws Exception { String schema = "create table books" + " (cash integer default 23 not null," + " title varchar(3) default 'foo'," + " PRIMARY KEY(cash));" + "PARTITION TABLE books ON COLUMN cash;" + "CREATE PROCEDURE FROM CLASS org.voltdb.compiler.procedures.AddBook;"; File schemaFile = VoltProjectBuilder.writeStringToTempFile(schema); { // valid use case Configuration c1 = new Configuration( new String[]{"initialize", "force", "voltdbroot", rootDH.getPath(), "schema", schemaFile.getPath()}); ServerThread server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); expectSimulatedExit(0); validateStagedCatalog(schema, null); clearCrash(); } try { // second attempt is not valid due to existing artifacts new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "schema", schemaFile.getPath()}); } catch (SimulatedExitException e){ assertEquals(e.getStatus(), -1); } assertEquals(true, schemaFile.delete()); } /** Test that a valid schema with no procedures can be used to stage a matching catalog. * @throws Exception upon failure or error */ @Test public void testInitWithSchemaInvalidJunkSchema() throws Exception { File schemaFile = Files.createTempDirectory("junk").toFile(); try { new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "force", "schema", schemaFile.getPath()}); fail("did not detect unusable schema file"); } catch (VoltDB.SimulatedExitException e) { assertEquals(e.getStatus(), -1); } assertEquals(true, schemaFile.delete()); } /** Test that a valid schema with no procedures can be used to stage a matching catalog. * @throws Exception upon failure or error */ @Test public void testInitWithSchemaInvalidMissingClass() throws Exception { String schema = "CREATE TABLE unicorns" + " (horn_size integer DEFAULT 12 NOT NULL," + " name varchar(32) DEFAULT 'Pixie' NOT NULL," + " PRIMARY KEY(name));" + "PARTITION TABLE unicorns ON COLUMN name;" + "CREATE PROCEDURE FROM CLASS org.voltdb.unicorns.ComputeSocialStanding;"; File schemaFile = VoltProjectBuilder.writeStringToTempFile(schema); Configuration c1 = new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "force", "schema", schemaFile.getPath()}); ServerThread server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); assertNotNull(serverException.get()); assertTrue(serverException.get().getMessage().equals("Faux crash of VoltDB successful.")); assertTrue(VoltDB.wasCrashCalled); assertTrue(VoltDB.crashMessage.contains("Could not compile specified schema")); assertEquals(true, schemaFile.delete()); } /** Tests that when there are base classes and non-class files in the stored procedures, * that these also exist in the staged catalog. * @throws Exception upon failure or error */ @Test public void testInitWithClassesAndArtifacts() throws Exception { System.out.println("Loading the schema from testprocs"); File resource = new File("tests/testprocs/org/voltdb_testprocs/fakeusecase/greetings/ddl.sql"); InputStream schemaReader = new FileInputStream(resource); assertNotNull("Could not find " + resource, schemaReader); String schema = CharStreams.toString(new InputStreamReader(schemaReader)); System.out.println("Creating a .jar file using all of the classes associated with this test."); InMemoryJarfile originalInMemoryJar = new InMemoryJarfile(); VoltCompiler compiler = new VoltCompiler(false, false); ClassPath classpath = ClassPath.from(this.getClass().getClassLoader()); String packageName = "org.voltdb_testprocs.fakeusecase.greetings"; int classesFound = 0; for (ClassInfo myclass : classpath.getTopLevelClassesRecursive(packageName)) { compiler.addClassToJar(originalInMemoryJar, myclass.load()); classesFound++; } // check that classes were found and loaded. If another test modifies "fakeusecase.greetings" it should modify this assert also. assertEquals(5, classesFound); System.out.println("Writing " + classesFound + " classes to jar file"); File classesJarfile = File.createTempFile("TestInitStartWithClasses-procedures", ".jar"); classesJarfile.deleteOnExit(); originalInMemoryJar.writeToFile(classesJarfile); Configuration c1 = new Configuration( new String[]{"initialize", "voltdbroot", rootDH.getPath(), "force", "schema", resource.getPath(), "classes", classesJarfile.getPath()}); ServerThread server = new ServerThread(c1); server.setUncaughtExceptionHandler(handleUncaught); server.start(); server.join(); validateStagedCatalog(schema, originalInMemoryJar); } /* For 'voltdb start' test coverage see TestStartWithSchema and (in Pro) TestStartWithSchemaAndDurability. */ }