/* * JBoss, Home of Professional Open Source. * Copyright 2015, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.test.patching; import static org.jboss.as.patching.IoUtils.newFile; import static org.jboss.as.test.patching.PatchingTestUtil.BASE_MODULE_DIRECTORY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.jboss.as.cli.CommandContext; import org.jboss.as.cli.CommandContextFactory; import org.jboss.as.cli.CommandLineException; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.patching.Constants; import org.jboss.as.patching.HashUtils; import org.jboss.as.patching.IoUtils; import org.jboss.as.test.patching.util.module.Module; import org.jboss.as.test.shared.TestSuiteEnvironment; import org.jboss.dmr.ModelNode; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.wildfly.core.testrunner.ServerControl; import org.wildfly.core.testrunner.WildflyTestRunner; /** * * @author Alexey Loubyansky */ @RunWith(WildflyTestRunner.class) @ServerControl(manual = true) public class PatchStreamsUnitTestCase extends PatchInfoTestBase { private static final File LAYERS_CONF = new File(PatchingTestUtil.MODULES_DIRECTORY, "layers.conf"); private static final File LAYERS_CONF_COPY = new File(PatchingTestUtil.MODULES_DIRECTORY, "layers.conf-copy"); private ByteArrayOutputStream bytesOs; private CLIOutputReader reader = new CLIOutputReader(); private File moduleADir; private File moduleBDir; private File moduleCDir; private CommandContext cli; @BeforeClass public static void prepareInstallation() throws Exception { installLayers("layerA", "layerB", "layerC"); } @AfterClass public static void restoreInstallation() throws Exception { uninstallLayers("layerA", "layerB", "layerC"); } @Before public void setup() throws Exception { bytesOs = new ByteArrayOutputStream(); moduleADir = createModule("layerA"); moduleBDir = createModule("layerB"); moduleCDir = createModule("layerC"); // to avoid the need to reset the terminal manually after the tests, e.g. 'stty sane' System.setProperty("aesh.terminal", "org.jboss.aesh.terminal.TestTerminal"); cli = CommandContextFactory.getInstance().newCommandContext(null, null, null, System.in, bytesOs); final ModelControllerClient client = ModelControllerClient.Factory.create(TestSuiteEnvironment.getServerAddress(), TestSuiteEnvironment.getServerPort()); cli.bindClient(client); } @After public void cleanup() throws Exception { if(bytesOs != null) { bytesOs = null; } if(cli != null) { cli.terminateSession(); } } @Override protected void rollbackAllPatches() throws Exception { // aging out overlays makes it impossible to rollback past a certain point // so no rolling back here final File home = new File(PatchingTestUtil.AS_DISTRIBUTION); PatchingTestUtil.resetInstallationState(home, BASE_MODULE_DIRECTORY); } @Test public void testMain() throws Exception { byte[] aHash = HashUtils.hashFile(moduleADir); byte[] bHash = HashUtils.hashFile(moduleBDir); byte[] cHash = HashUtils.hashFile(moduleCDir); aHash = applyPatch("ProductA", "1.0.0.Beta1", "1.0.0", "A_1.0.0", "layerA", "layerA_1.0.0", aHash); aHash = applyPatch("ProductA", "1.0.0", null, "A_ONE_OFF_0", "layerA", "layerA_ONE_OFF_0", aHash); bHash = applyPatch("ProductB", "2.0.0.Beta2", "2.0.0", "B_2.0.0", "layerB", "layerB_2.0.0", bHash); bHash = applyPatch("ProductB", "2.0.0", null, "B_ONE_OFF_0", "layerB", "layerB_ONE_OFF_0", bHash); cHash = applyPatch("ProductC", "3.0.0.Beta3", "3.0.0", "C_3.0.0", "layerC", "layerC_3.0.0", cHash); cHash = applyPatch("ProductC", "3.0.0", null, "C_ONE_OFF_0", "layerC", "layerC_ONE_OFF_0", cHash); aHash = applyPatch("ProductA", "1.0.0", "1.0.1", "A_CP1", "layerA", "layerA_CP1", aHash); aHash = applyPatch("ProductA", "1.0.1", null, "A_ONE_OFF_1", "layerA", "layerA_ONE_OFF_1", aHash); bHash = applyPatch("ProductB", "2.0.0", "2.0.1", "B_CP1", "layerB", "layerB_CP1", bHash); aHash = applyPatch("ProductA", "1.0.1", null, "A_ONE_OFF_2", "layerA", "layerA_ONE_OFF_2", aHash); bHash = applyPatch("ProductB", "2.0.1", null, "B_ONE_OFF_1", "layerB", "layerB_ONE_OFF_1", bHash); cHash = applyPatch("ProductC", "3.0.0", "3.0.1", "C_CP1", "layerC", "layerC_CP1", cHash); cHash = applyPatch("ProductC", "3.0.1", null, "C_ONE_OFF_1", "layerC", "layerC_ONE_OFF_1", cHash); bHash = applyPatch("ProductB", "2.0.1", null, "B_ONE_OFF_2", "layerB", "layerB_ONE_OFF_2", bHash); cHash = applyPatch("ProductC", "3.0.1", null, "C_ONE_OFF_2", "layerC", "layerC_ONE_OFF_2", cHash); controller.start(); try { assertPatchStreamNames(productConfig.getProductName(), "ProductA", "ProductB", "ProductC"); assertPatches("ProductA", "1.0.1", "A_CP1", "A_ONE_OFF_2", "A_ONE_OFF_1"); assertPatches("ProductB", "2.0.1", "B_CP1", "B_ONE_OFF_2", "B_ONE_OFF_1"); assertPatches("ProductC", "3.0.1", "C_CP1", "C_ONE_OFF_2", "C_ONE_OFF_1"); } finally { controller.stop(); } rollback("ProductA", "A_ONE_OFF_2"); rollbackLast("ProductB"); controller.start(); try { assertPatchStreamNames(productConfig.getProductName(), "ProductA", "ProductB", "ProductC"); assertPatches("ProductA", "1.0.1", "A_CP1", "A_ONE_OFF_1"); assertPatches("ProductB", "2.0.1", "B_CP1", "B_ONE_OFF_1"); assertPatches("ProductC", "3.0.1", "C_CP1", "C_ONE_OFF_2", "C_ONE_OFF_1"); assertHistory("ProductA", new HistoryEntry("A_ONE_OFF_1", true), new HistoryEntry("A_CP1", false), new HistoryEntry("A_ONE_OFF_0", true), new HistoryEntry("A_1.0.0", false)); assertHistory("ProductB", new HistoryEntry("B_ONE_OFF_1", true), new HistoryEntry("B_CP1", false), new HistoryEntry("B_ONE_OFF_0", true), new HistoryEntry("B_2.0.0", false)); assertHistory("ProductC", new HistoryEntry("C_ONE_OFF_2", true), new HistoryEntry("C_ONE_OFF_1", true), new HistoryEntry("C_CP1", false), new HistoryEntry("C_ONE_OFF_0", true), new HistoryEntry("C_3.0.0", false)); assertOverlays("layerA", true, "layerA_ONE_OFF_1", "layerA_CP1", "layerA_ONE_OFF_0", "layerA_1.0.0"); assertOverlays("layerB", true, "layerB_ONE_OFF_1", "layerB_CP1", "layerB_ONE_OFF_0", "layerB_2.0.0"); assertOverlays("layerC", true, "layerC_ONE_OFF_1", "layerC_CP1", "layerC_ONE_OFF_0", "layerC_3.0.0"); // ageout overlays for specific stream cliHandle("/core-service=patching/patch-stream=ProductA:ageout-history"); assertOverlays("layerA", false, "layerA_ONE_OFF_0", "layerA_1.0.0"); assertOverlays("layerA", true, "layerA_ONE_OFF_1", "layerA_CP1"); assertOverlays("layerB", true, "layerB_ONE_OFF_1", "layerB_CP1", "layerB_ONE_OFF_0", "layerB_2.0.0"); assertOverlays("layerC", true, "layerC_ONE_OFF_1", "layerC_CP1", "layerC_ONE_OFF_0", "layerC_3.0.0"); // ageout overlays for all the rest cliHandle("/core-service=patching:ageout-history"); assertOverlays("layerA", false, "layerA_ONE_OFF_0", "layerA_1.0.0"); assertOverlays("layerA", true, "layerA_ONE_OFF_1", "layerA_CP1"); assertOverlays("layerB", false, "layerB_ONE_OFF_0", "layerB_2.0.0"); assertOverlays("layerB", true, "layerB_ONE_OFF_1", "layerB_CP1"); assertOverlays("layerC", false, "layerC_ONE_OFF_0", "layerC_3.0.0"); assertOverlays("layerC", true, "layerC_ONE_OFF_1", "layerC_CP1"); } finally { controller.stop(); } } protected void assertHistory(final String stream, HistoryEntry... entries) throws Exception { cliHandle("patch history --patch-stream=" + stream); final List<ModelNode> list = ModelNode.fromJSONString(reader.readOutput()).get("result").asList(); assertEquals(entries.length, list.size()); for(int i = 0; i < entries.length; ++i) { final HistoryEntry entry = entries[i]; final ModelNode item = list.get(i); assertEquals(entry.patchId, item.get("patch-id").asString()); assertEquals(entry.oneOff ? "one-off" : "cumulative", item.get("type").asString()); assertTrue(item.hasDefined("applied-at")); } } protected void rollback(final String stream, final String patchId) throws CommandLineException { String line = "patch rollback --reset-configuration=true --patch-stream=" + stream; if(patchId != null) { line += " --patch-id=" + patchId; } controller.start(); try { cliHandle(line); } finally { controller.stop(); } } protected void rollbackLast(final String stream) throws CommandLineException { rollback(stream, null); } protected void assertPatches(String stream, String version, String cp, String... oneOffs) throws Exception { cliHandle("patch info --patch-stream=" + stream); final Map<String, String> result = reader.readTable(); assertEquals(version, result.get("Version")); assertEquals(cp, result.get("Cumulative patch ID")); final StringBuilder buf = new StringBuilder(); if (oneOffs.length > 0) { buf.append(oneOffs[0]); for (int i = 1; i < oneOffs.length; ++i) { buf.append(',').append(oneOffs[i]); } } assertEquals(buf.toString(), result.get("One-off patches")); } protected void assertPatchStreamNames(String... names) throws CommandLineException { cliHandle("/core-service=patching:read-children-names(child-type=patch-stream)"); ModelNode node = ModelNode.fromString(reader.readOutput()); final List<String> streams = new ArrayList<String>(names.length); for(ModelNode item : node.get("result").asList()) { streams.add(item.asString()); } assertEquals(names.length, streams.size()); for(String name : names) { assertTrue(streams.contains(name)); } } protected void cliHandle(String line) throws CommandLineException { bytesOs.reset(); cli.handle(line); reader.refresh(); } protected File createModule(String targetLayer) throws IOException { final Module module = new Module.Builder("module-test"). miscFile(new ResourceItem("resource-test", ("module resource").getBytes(StandardCharsets.UTF_8))). build(); return module.writeToDisk(new File(PatchingTestUtil.LAYERS_DIRECTORY, targetLayer)); } private static void installLayers(String... layers) throws IOException { installLayers(false, layers); } private static void installLayers(boolean excludeBase, String... layers) throws IOException { for (String layer : layers) { IoUtils.mkdir(PatchingTestUtil.LAYERS_DIRECTORY, layer); } final Properties props = new Properties(); final StringBuilder str = new StringBuilder(); for (int i = 0; i < layers.length; i++) { if (i > 0) { str.append(','); } str.append(layers[i]); } props.put(Constants.LAYERS, str.toString()); props.put(Constants.EXCLUDE_LAYER_BASE, String.valueOf(excludeBase)); if(LAYERS_CONF.exists()) { IoUtils.copy(LAYERS_CONF, LAYERS_CONF_COPY); } final FileOutputStream os = new FileOutputStream(LAYERS_CONF); try { props.store(os, ""); } finally { IoUtils.safeClose(os); } } private static void uninstallLayers(String... layers) throws IOException { for (String layer : layers) { IoUtils.recursiveDelete(IoUtils.newFile(PatchingTestUtil.LAYERS_DIRECTORY, layer)); } if(LAYERS_CONF_COPY.exists()) { IoUtils.copy(LAYERS_CONF_COPY, LAYERS_CONF); } else { IoUtils.recursiveDelete(LAYERS_CONF); } } protected void assertOverlays(final String layer, boolean exist, final String... patches) { final File base = newFile(PatchingTestUtil.LAYERS_DIRECTORY, layer); final File overlays = new File(base, ".overlays"); for (final String patch : patches) { final File overlay = new File(overlays, patch); assertEquals("Overlay for layer " + layer + " patch " + patch + (exist ? " exists" : " does not exist"), exist, overlay.exists()); } } class CLIOutputReader { String line; BufferedReader reader; CLIOutputReader() { } void refresh() { if(bytesOs.size() > 0) { reader = new BufferedReader(new StringReader(new String(bytesOs.toByteArray(), StandardCharsets.UTF_8))); } else { reader = null; } } protected String readOutput() { return new String(bytesOs.toByteArray(), StandardCharsets.UTF_8); } protected Map<String, String> readTable() throws IOException { String line = reader.readLine(); if(line == null) { return null; } if(line.isEmpty()) { return Collections.emptyMap(); } final Map<String, String> map = new HashMap<String,String>(); addLine(map, line); while(!(line == null || line.isEmpty())) { addLine(map, line); line = reader.readLine(); } return map; } protected void addLine(Map<String, String> map, String line) { final int i = line.indexOf(':'); if(i < 0) { map.put(line, null); } else { final String key = line.substring(0, i).trim(); final String value; if(i + 1 < line.length()) { value = line.substring(i + 1, line.length()).trim(); } else { value = ""; } map.put(key, value); } } } static class HistoryEntry { final String patchId; final boolean oneOff; HistoryEntry(String patchId, boolean oneOff) { this.patchId = patchId; this.oneOff = oneOff; } } }