blob: 12be426b2a6adc435f9114d73c526f1cd5f7d660 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.api;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.management.Notification;
import org.apache.cassandra.distributed.shared.AssertUtils;
public class NodeToolResult
{
private final String[] commandAndArgs;
private final int rc;
private final List<Notification> notifications;
private final Throwable error;
private final String stdout;
private final String stderr;
public NodeToolResult(String[] commandAndArgs, int rc, List<Notification> notifications, Throwable error)
{
this(commandAndArgs, rc, notifications, error, null, null);
}
public NodeToolResult(String[] commandAndArgs, int rc, List<Notification> notifications, Throwable error, String stdout, String stderr)
{
this.commandAndArgs = commandAndArgs;
this.rc = rc;
this.notifications = notifications;
this.error = error;
this.stdout = stdout;
this.stderr = stderr;
}
public String[] getCommandAndArgs()
{
return commandAndArgs;
}
public int getRc()
{
return rc;
}
public List<Notification> getNotifications()
{
return notifications;
}
public Throwable getError()
{
return error;
}
public String getStdout()
{
return stdout;
}
public String getStderr()
{
return stderr;
}
public Asserts asserts()
{
return new Asserts();
}
public final class Asserts
{
public Asserts success()
{
if (rc != 0)
fail("was not successful");
return this;
}
public Asserts failure()
{
if (rc == 0)
fail("was successful but not expected to be");
return this;
}
public Asserts errorContains(String msg)
{
return errorContainsAny(msg);
}
public Asserts errorContainsAny(String... messages)
{
AssertUtils.assertNotEquals("no error messages defined to check against", 0, messages.length);
AssertUtils.assertNotNull("No exception was found but expected one", error);
if (!Stream.of(messages).anyMatch(msg -> error.getMessage().contains(msg)))
fail("Error message '" + error.getMessage() + "' does not contain any of " + Arrays.toString(messages));
return this;
}
public Asserts notificationContains(String msg)
{
AssertUtils.assertNotNull("notifications not defined", notifications);
AssertUtils.assertFalse("notifications not defined", notifications.isEmpty());
for (Notification n : notifications)
{
if (n.getMessage().contains(msg))
{
return this;
}
}
fail("Unable to locate message " + msg + " in notifications: " + NodeToolResult.toString(notifications));
return this; // unreachable
}
public Asserts notificationContains(ProgressEventType type, String msg)
{
int userType = type.ordinal();
AssertUtils.assertNotNull("notifications not defined", notifications);
AssertUtils.assertFalse("notifications not defined", notifications.isEmpty());
for (Notification n : notifications)
{
if (notificationType(n) == userType)
{
if (n.getMessage().contains(msg))
{
return this;
}
}
}
fail("Unable to locate message '" + msg + "' in notifications: " + NodeToolResult.toString(notifications));
return this; // unreachable
}
public Asserts stdoutContains(String substring)
{
return assertConsoleOutput(substring, true, true);
}
public Asserts stderrContains(String substring)
{
return assertConsoleOutput(substring, false, true);
}
public Asserts stdoutNotContains(String substring)
{
return assertConsoleOutput(substring, true, false);
}
public Asserts stderrNotContains(String substring)
{
return assertConsoleOutput(substring, false, false);
}
private Asserts assertConsoleOutput(String substring, boolean isStdout, boolean assertContains)
{
String name = isStdout ? "stdout" : "stderr";
String output = isStdout ? stdout : stderr;
AssertUtils.assertNotNull(name + " not defined", output);
if (assertContains)
{
if (output.contains(substring))
{
return this;
}
fail("Unable to locate substring '" + substring + "' in " + name + ": " + output);
}
else
{
if (!output.contains(substring))
{
return this;
}
fail("Found unexpected substring '" + substring + "' in " + name + ": " + output);
}
return this; // unreachable
}
private void fail(String message)
{
StringBuilder sb = new StringBuilder();
sb.append("nodetool command ").append(Arrays.toString(commandAndArgs)).append(" ").append(message).append("\n");
if (stdout != null)
sb.append("stdout:\n").append(stdout).append("\n");
if (stderr != null)
sb.append("stderr:\n").append(stderr).append("\n");
sb.append("Notifications:\n");
for (Notification n : notifications)
sb.append(NodeToolResult.toString(n)).append("\n");
if (error != null)
sb.append("Error:\n").append(getStackTraceAsString(error)).append("\n");
throw new AssertionError(sb.toString());
}
}
private static String getStackTraceAsString(Throwable throwable)
{
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
private static String toString(Collection<Notification> notifications)
{
return notifications.stream().map(NodeToolResult::toString).collect(Collectors.joining(", "));
}
private static String toString(Notification notification)
{
ProgressEventType type = ProgressEventType.values()[notificationType(notification)];
String msg = notification.getMessage();
Object src = notification.getSource();
return "Notification{" +
"type=" + type +
", src=" + src +
", message=" + msg +
"}";
}
private static int notificationType(Notification n)
{
return ((Map<String, Integer>) n.getUserData()).get("type").intValue();
}
public String toString()
{
return "NodeToolResult{" +
"commandAndArgs=" + Arrays.toString(commandAndArgs) +
", rc=" + rc +
", notifications=[" + notifications.stream().map(n -> ProgressEventType.values()[notificationType(n)].name()).collect(Collectors.joining(", ")) + "]" +
", error=" + error +
'}';
}
/**
* Progress event type.
*
* <p>
* Progress starts by emitting {@link #START}, followed by emitting zero or more {@link #PROGRESS} events,
* then it emits either one of {@link #ERROR}/{@link #ABORT}/{@link #SUCCESS}.
* Progress indicates its completion by emitting {@link #COMPLETE} at the end of process.
* </p>
* <p>
* {@link #NOTIFICATION} event type is used to just notify message without progress.
* </p>
*/
public enum ProgressEventType
{
/**
* Fired first when progress starts.
* Happens only once.
*/
START,
/**
* Fire when progress happens.
* This can be zero or more time after START.
*/
PROGRESS,
/**
* When observing process completes with error, this is sent once before COMPLETE.
*/
ERROR,
/**
* When observing process is aborted by user, this is sent once before COMPLETE.
*/
ABORT,
/**
* When observing process completes successfully, this is sent once before COMPLETE.
*/
SUCCESS,
/**
* Fire when progress complete.
* This is fired once, after ERROR/ABORT/SUCCESS is fired.
* After this, no more ProgressEvent should be fired for the same event.
*/
COMPLETE,
/**
* Used when sending message without progress.
*/
NOTIFICATION
}
}