/*
 * 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.brooklyn.entity.software.base.test.location;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

/**
 * Tests execution of commands (batch and powershell) on Windows over WinRM, and of
 * file upload.
 * 
 * There are limitations with what is supported by winrm4j. These are highlighted in
 * tests marked as "WIP" (see individual tests).
 * 
 * These limitations are documented in docs/guide/yaml/winrm/index.md.
 * Please update the docs if you encountered new situations, or change the behaviuor 
 * of existing use-cases.
 */

public class WinRmMachineLocationLiveTest {
    private static final int MAX_EXECUTOR_THREADS = 100;

    /*
     * TODO: Deferred implementing copyFrom or environment variables.
     */
    
    private static final Logger LOG = LoggerFactory.getLogger(WinRmMachineLocationLiveTest.class);

    private static final String INVALID_CMD = "thisCommandDoesNotExistAEFafiee3d";
    private static final String PS_ERR_ACTION_PREF_EQ_STOP = "$ErrorActionPreference = \"Stop\"";

    protected MachineProvisioningLocation<WinRmMachineLocation> loc;
    protected TestApplication app;
    protected ManagementContextInternal mgmt;

    protected WinRmMachineLocation machine;
    
    private ListeningExecutorService executor;
    
    @BeforeClass(alwaysRun=true)
    public void setUpClass() throws Exception {
        executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(MAX_EXECUTOR_THREADS));
        
        mgmt = new LocalManagementContextForTests(BrooklynProperties.Factory.newDefault());
        
        loc = newLoc(mgmt);
        machine = loc.obtain(ImmutableMap.of());
        
        LOG.info("PROVISIONED: "+machine.getAddress()+":"+machine.getPort()
                +", "+machine.getUser()+":"+machine.config().get(WinRmMachineLocation.PASSWORD));
    }
    
    @AfterClass(alwaysRun=true)
    public void tearDownClass() throws Exception {
        try {
            if (executor != null) executor.shutdownNow();
            if (machine != null) loc.release(machine);
            if (mgmt != null) Entities.destroyAll(mgmt);
        } catch (Throwable t) {
            LOG.error("Caught exception in tearDown method", t);
        } finally {
            executor = null;
            mgmt = null;
        }
    }

    /**
     * Returns a location for obtaining a single WinRM machine. This method will be called once during 
     * {@code @BeforeClass}, then {@code loc.obtain()} will be called. The obtained machine will be
     * released in {@code @AfterClass}. 
     */
    protected MachineProvisioningLocation<WinRmMachineLocation> newLoc(ManagementContextInternal mgmt) throws Exception {
        return WindowsTestFixture.setUpWindowsLocation(mgmt);
    }
    
    @Test(groups="Live")
    public void testCopyTo() throws Exception {
        String contents = "abcdef";
        runCopyTo(contents);
        runCopyFileTo(contents);
    }
    
    // Takes several minutes to upload/download!
    @Test(groups="Live")
    public void testLargeFileCopyTo() throws Exception {
        String contents = Identifiers.makeRandomId(65537);
        runCopyTo(contents);
        runCopyFileTo(contents);
    }
    
    protected void runCopyTo(String contents) throws Exception {
        String remotePath = "C:\\myfile-"+Identifiers.makeRandomId(4)+".txt";
        machine.copyTo(new ByteArrayInputStream(contents.getBytes()), remotePath);
        
        WinRmToolResponse response = machine.executeCommand("type "+remotePath);
        String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
        assertEquals(response.getStatusCode(), 0, msg);
        assertEquals(response.getStdOut().trim(), contents, msg);
    }
    
    protected void runCopyFileTo(String contents) throws Exception {
        File localFile = File.createTempFile("winrmtest"+Identifiers.makeRandomId(4), ".txt");
        try {
            Files.write(contents, localFile, Charsets.UTF_8);
            String remotePath = "C:\\myfile-"+Identifiers.makeRandomId(4)+".txt";
            machine.copyTo(localFile, remotePath);
            
            WinRmToolResponse response = machine.executeCommand("type "+remotePath);
            String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
            assertEquals(response.getStatusCode(), 0, msg);
            assertEquals(response.getStdOut().trim(), contents, msg);
        } finally {
            localFile.delete();
        }
    }
    
    @Test(groups="Live")
    public void testExecScript() throws Exception {
        assertExecSucceeds("echo myline", "myline", "");
    }
    
    /*
     * TODO Not supported in winrm4j (or PyWinRM).
     * 
     * Just gives "first", and exit code 1.
     */
    @Test(groups={"Live", "WIP"}, enabled=false)
    public void testExecMultiLineScript() throws Exception {
        assertExecSucceeds("echo first" + "\r\n" + "echo second", "first"+"\r\n"+"second", "");
    }
    
    @Test(groups={"Live"})
    public void testExecMultiPartScript() throws Exception {
        assertExecSucceeds(ImmutableList.of("echo first", "echo second"), "first "+"\r\n"+"second", "");
    }
    
    @Test(groups="Live")
    public void testExecFailingScript() throws Exception {
        final String INVALID_CMD = "thisCommandDoesNotExistAEFafiee3d";
        
        // Single commands
        assertExecFails(INVALID_CMD);
        assertExecFails(ImmutableList.of(INVALID_CMD));
    }

    @Test(groups="Live")
    public void testExecScriptExit0() throws Exception {
        assertExecSucceeds("exit /B 0", "", "");
        assertExecSucceeds(ImmutableList.of("exit /B 0"), "", "");
    }

    /*
     * TODO Not supported in winrm4j (or PyWinRM).
     * 
     * Executing (in python):
     *     import winrm
     *     s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
     *     r = s.run_cmd("exit /B 1")
     * gives exit code 0.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecScriptExit1() throws Exception {
        // Single commands
        assertExecFails("exit /B 1");
        assertExecFails(ImmutableList.of("exit /B 1"));
    }

    @Test(groups="Live")
    public void testExecBatchFileSingleLine() throws Exception {
        String script = "EXIT /B 0";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecSucceeds(scriptPath, null, "");
    }

    @Test(groups="Live")
    public void testExecBatchFileMultiLine() throws Exception {
        String script = Joiner.on("\n").join(
                "@ECHO OFF",
                "echo first", 
                "echo second", 
                "EXIT /B 0");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecSucceeds(scriptPath, "first"+"\r\n"+"second", "");
    }

    @Test(groups="Live")
    public void testExecBatchFileWithArgs() throws Exception {
        String script = Joiner.on("\n").join(
                "@ECHO OFF",
                "echo got %1", 
                "echo got %2", 
                "EXIT /B 0");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecSucceeds(scriptPath+" first second", "got first"+"\r\n"+"got second", "");
    }

    @Test(groups="Live")
    public void testExecBatchFileWithExit1() throws Exception {
        String script = "EXIT /B 1";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecFails(scriptPath);
    }

    @Test(groups="Live")
    public void testExecCorruptExe() throws Exception {
        String exe = "garbage";
        String exePath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".exe";
        machine.copyTo(new ByteArrayInputStream(exe.getBytes()), exePath);

        assertExecFails(exePath);
    }

    @Test(groups="Live")
    public void testExecFilePs() throws Exception {
        String script = Joiner.on("\r\n").join(
                "Write-Host myline", 
                "exit 0");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsSucceeds(
                "PowerShell -NonInteractive -NoProfile -Command "+scriptPath,
                "myline",
                "");
    }

    @Test(groups="Live")
    public void testExecFilePsWithExit1() throws Exception {
        String script = Joiner.on("\r\n").join(
                "Write-Host myline", 
                "exit 1");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecFails("PowerShell -NonInteractive -NoProfile -Command "+scriptPath);
    }

    /*
     * TODO Not supported in PyWinRM - single line .ps1 file with "exit 1" gives an
     * exit code 0 over PyWinRM, but an exit code 1 when executed locally!
     * 
     * Executing (in python):
     *     import winrm
     *     s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
     *     r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineExit1.ps1")
     * gives exit code 0.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecFilePsWithSingleLineExit1() throws Exception {
        String script = "exit 1";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecFails("PowerShell -NonInteractive -NoProfile -Command "+scriptPath);
    }

    @Test(groups="Live")
    public void testExecPsScript() throws Exception {
        assertExecPsSucceeds("Write-Host myline", "myline", "");
    }
    
    @Test(groups="Live")
    public void testExecPsMultiLineScript() throws Exception {
        // Note stdout is "\n" rather than "\r\n" (the latter is returned for run_cmd, versus run_ps)
        assertExecPsSucceeds("Write-Host first" + "\r\n" + "Write-Host second", "first"+"\n"+"second", "");
    }
    
    @Test(groups="Live")
    public void testExecPsMultiLineScriptWithoutSlashR() throws Exception {
        assertExecPsSucceeds("Write-Host first" + "\n" + "Write-Host second", "first"+"\n"+"second", "");
    }
    
    @Test(groups="Live")
    public void testExecPsMultiPartScript() throws Exception {
        assertExecPsSucceeds(ImmutableList.of("Write-Host first", "Write-Host second"), "first"+"\n"+"second", "");
    }

    @Test(groups="Live")
    public void testExecPsBatchFile() throws Exception {
        String script = "EXIT /B 0";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsSucceeds("& '"+scriptPath+"'", null, "");
    }
    
    @Test(groups="Live")
    public void testExecPsBatchFileExit1() throws Exception {
        String script = "EXIT /B 1";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsFails("& '"+scriptPath+"'");
    }

    /*
     * TODO Not supported in PyWinRM - gives exit status 1, rather than the 3 from the batch file.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecPsBatchFileExit3() throws Exception {
        String script = "EXIT /B 3";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        WinRmToolResponse response = machine.executePsScript("& '"+scriptPath+"'");
        String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
        assertEquals(response.getStatusCode(), 3, msg);
    }

    @Test(groups="Live")
    public void testExecPsCorruptExe() throws Exception {
        String exe = "garbage";
        String exePath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".exe";
        machine.copyTo(new ByteArrayInputStream(exe.getBytes()), exePath);

        assertExecPsFails("& '"+exePath+"'");
    }

    @Test(groups="Live")
    public void testExecPsFileWithArg() throws Exception {
        String script = Joiner.on("\r\n").join(
                "Param(",
                "  [string]$myarg",
                ")",
                "Write-Host got $myarg", 
                "exit 0");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsSucceeds("& "+scriptPath+" -myarg myval", "got myval", "");
    }

    @Test(groups="Live")
    public void testExecPsFilePs() throws Exception {
        String script = Joiner.on("\r\n").join(
                "Write-Host myline", 
                "exit 0");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsSucceeds("& "+scriptPath, "myline", "");
    }

    @Test(groups="Live")
    public void testExecPsFilePsWithExit1() throws Exception {
        String script = Joiner.on("\r\n").join(
                "Write-Host myline", 
                "exit 1");
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
        System.out.println(scriptPath);

        assertExecPsFails("& "+scriptPath);
    }

    /*
     * TODO Not supported in PyWinRM - single line .ps1 file with "exit 1" gives an
     * exit code 0 over PyWinRM, but an exit code 1 when executed locally!
     * 
     * Executing (in python):
     *     import winrm
     *     s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
     *     r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineExit1.ps1")
     * gives exit code 0.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecPsFilePsSingleLineWithExit1() throws Exception {
        String script = "exit 1";
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsFails("& "+scriptPath);
    }

    /*
     * TODO Not supported in winrm4j - single line .ps1 file with "exit 1" gives an
     * exit code 0 over PyWinRM, but an exit code 1 when executed locally!
     * 
     * Executing (in python):
     *     import winrm
     *     s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
     *     r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineGarbage.ps1")
     * gives exit code 0.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecPsFilePsSingleLineWithInvalidCommand() throws Exception {
        String script = INVALID_CMD;
        String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
        machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);

        assertExecPsFails("& "+scriptPath);
    }

    @Test(groups="Live")
    public void testConfirmUseOfErrorActionPreferenceDoesNotCauseErr() throws Exception {
        // Confirm that ErrorActionPreference=Stop does not itself cause a failure, and still get output on success.
        assertExecPsSucceeds(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline"), "myline", "");
    }

    /*
     * TODO Not supported in PyWinRM
     * 
     * Executing (in python):
     *     import winrm
     *     s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
     *     r = s.run_ps("exit 1")
     * gives exit code 0.
     */
    @Test(groups={"Live", "WIP"})
    public void testExecPsExit1() throws Exception {
        // Single commands
        assertExecPsFails("exit 1");
        assertExecPsFails(ImmutableList.of("exit 1"));
        
        // Multi-part
        assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline", "exit 1"));
        
        // Multi-line
        assertExecPsFails(PS_ERR_ACTION_PREF_EQ_STOP + "\n" + "Write-Host myline" + "\n" + "exit 1");
    }

    @Test(groups="Live")
    public void testExecFailingPsScript() throws Exception {
        // Single commands
        assertExecPsFails(INVALID_CMD);
        assertExecPsFails(ImmutableList.of(INVALID_CMD));
        
        // Multi-part commands
        assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline", INVALID_CMD));
        assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, INVALID_CMD, "Write-Host myline"));
    }

    @Test(groups={"Live", "Acceptance"})
    public void testExecConcurrently() throws Exception {
        final int NUM_RUNS = 10;
        final int TIMEOUT_MINS = 30;
        final AtomicInteger counter = new AtomicInteger();
        
        // Find the test methods that are enabled, and that are not WIP 
        List<Method> methodsToRun = Lists.newArrayList();
        Method[] allmethods = WinRmMachineLocationLiveTest.class.getMethods();
        for (Method method : allmethods) {
            Test annotatn = method.getAnnotation(Test.class);
            if (method.getParameterTypes().length != 0) {
                continue;
            }
            if (method.getName().equals("testExecConcurrently")) {
                continue;
            }
            if (annotatn == null || !annotatn.enabled()) {
                continue;
            }
            String[] groups = annotatn.groups();
            if (groups != null && Arrays.asList(groups).contains("WIP")) {
                continue;
            }
            methodsToRun.add(method);
        }

        // Execute all the methods many times
        LOG.info("Executing "+methodsToRun.size()+" methods "+NUM_RUNS+" times each, with "+MAX_EXECUTOR_THREADS+" threads for concurrent execution; max permitted time "+TIMEOUT_MINS+"mins; methods="+methodsToRun);
        
        List<ListenableFuture<?>> results = Lists.newArrayList();
        for (int i = 0; i < NUM_RUNS; i++) {
            for (final Method method : methodsToRun) {
                results.add(executor.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        LOG.info("Executing "+method.getName()+" in thread "+Thread.currentThread());
                        Stopwatch stopwatch = Stopwatch.createStarted();
                        try {
                            method.invoke(WinRmMachineLocationLiveTest.this);
                            LOG.info("Executed "+method.getName()+" in "+Time.makeTimeStringRounded(stopwatch)+", in thread "+Thread.currentThread()+"; total "+counter.incrementAndGet()+" methods done");
                            return null;
                        } catch (Exception e) {
                            LOG.error("Execute failed for "+method.getName()+" after "+Time.makeTimeStringRounded(stopwatch)+", in thread "+Thread.currentThread()+"; total "+counter.incrementAndGet()+" methods done");
                            throw e;
                        }
                    }}));
            }
        }
        
        Futures.allAsList(results).get(TIMEOUT_MINS, TimeUnit.MINUTES);
    }

    private void assertExecFails(String cmd) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertFailed(cmd, machine.executeCommand(cmd), stopwatch);
    }

    private void assertExecFails(List<String> cmds) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertFailed(cmds, machine.executeCommand(cmds), stopwatch);
    }
    
    private void assertExecPsFails(String cmd) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertFailed(cmd, machine.executePsScript(cmd), stopwatch);
    }
    
    private void assertExecPsFails(List<String> cmds) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertFailed(cmds, machine.executePsScript(cmds), stopwatch);
    }

    private void assertExecSucceeds(String cmd, String stdout, String stderr) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertSucceeded(cmd, machine.executeCommand(cmd), stdout, stderr, stopwatch);
    }

    private void assertExecSucceeds(List<String> cmds, String stdout, String stderr) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertSucceeded(cmds, machine.executeCommand(cmds), stdout, stderr, stopwatch);
    }

    private void assertExecPsSucceeds(String cmd, String stdout, String stderr) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertSucceeded(cmd, machine.executePsScript(cmd), stdout, stderr, stopwatch);
    }

    private void assertExecPsSucceeds(List<String> cmds, String stdout, String stderr) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        assertSucceeded(cmds, machine.executePsScript(cmds), stdout, stderr, stopwatch);
    }

    private void assertFailed(Object cmd, WinRmToolResponse response, Stopwatch stopwatch) {
        String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
        LOG.info("Executed in "+Time.makeTimeStringRounded(stopwatch)+" (asserting failed): "+msg+"; cmd="+cmd);
        assertNotEquals(response.getStatusCode(), 0, msg);
    }
    
    private WinRmToolResponse assertSucceeded(Object cmd, WinRmToolResponse response, String stdout, String stderr, Stopwatch stopwatch) {
        String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
        LOG.info("Executed in "+Time.makeTimeStringRounded(stopwatch)+" (asserting success): "+msg+"; cmd="+cmd);
        assertEquals(response.getStatusCode(), 0, msg);
        if (stdout != null) assertEquals(response.getStdOut().trim(), stdout, msg);
        if (stderr != null) assertEquals(response.getStdErr().trim(), stderr, msg);
        return response;
    }
}
