com.intuit.karate.debug.DebugThread Maven / Gradle / Ivy
The newest version!
/*
* The MIT License
*
* Copyright 2022 Karate Labs 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 OR COPYRIGHT HOLDERS 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 com.intuit.karate.debug;
import com.intuit.karate.LogAppender;
import com.intuit.karate.core.*;
import com.intuit.karate.RuntimeHook;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author pthomas3
*/
public class DebugThread implements RuntimeHook, LogAppender {
private static final Logger logger = LoggerFactory.getLogger(DebugThread.class);
public final long id;
public final String name;
public final Thread thread;
public final Stack stack = new Stack();
private final Map stepModes = new HashMap();
public final DapServerHandler handler;
private boolean stepIn;
private boolean stepBack;
private boolean paused;
private boolean interrupted;
private boolean stopped;
private boolean errored;
private final String appenderPrefix;
private LogAppender appender = LogAppender.NO_OP;
public DebugThread(Thread thread, DapServerHandler handler) {
id = thread.getId();
name = thread.getName();
appenderPrefix = "[" + name + "] ";
this.thread = thread;
this.handler = handler;
}
protected void pause() {
paused = true;
}
private boolean stop(String reason) {
return stop(reason, null);
}
private boolean stop(String reason, String description) {
handler.stopEvent(id, reason, description);
stopped = true;
synchronized (this) {
try {
wait();
} catch (Exception e) {
logger.warn("thread error: {}", e.getMessage());
interrupted = true;
return false; // exit all the things
}
}
handler.continueEvent(id);
// if we reached here - we have "resumed"
// the stepBack logic is a little faulty and can only be called BEFORE beforeStep() (yes 2 befores)
if (stepBack) { // don't clear flag yet !
getContext().stepBack();
return false; // abort and do not execute step !
}
if (stopped) {
getContext().stepReset();
return false;
}
return true;
}
protected void resume() {
stopped = false;
handler.evaluatePreStep(getContext());
for (DebugThread dt : handler.THREADS.values()) {
synchronized (dt) {
dt.notify();
}
}
}
@Override
public boolean beforeScenario(ScenarioRuntime context) {
long frameId = handler.nextFrameId();
stack.push(frameId);
handler.FRAMES.put(frameId, context);
handler.FRAME_VARS.put(frameId, new Stack<>());
if (context.caller.depth == 0) {
handler.THREADS.put(id, this);
}
appender = context.getLogAppender();
context.logger.setAppender(this); // wrap
return true;
}
@Override
public void afterScenario(ScenarioRuntime context) {
stack.pop();
if (context.caller.depth == 0) {
handler.THREADS.remove(id);
}
context.logger.setAppender(appender); // unwrap
}
@Override
public boolean beforeStep(Step step, ScenarioRuntime context) {
if (interrupted) {
return false;
}
if (paused) {
paused = false;
return stop("pause");
} else if (errored) {
errored = false; // clear the flag else the debugger will never move past this step
if (isStepMode()) {
// allow user to skip this step even if it is broken
context.stepProceed();
return false;
} else {
// rewind and stop so that user can re-try this step after hot-fixing it
context.stepReset();
return false;
}
} else if (stepBack) {
stepBack = false;
return stop("step");
} else if (stepIn) {
stepIn = false;
return stop("step");
} else if (isStepMode()) {
return stop("step");
} else {
int line = step.getLine();
if (handler.isBreakpoint(step, line, context)) {
return stop("breakpoint");
} else {
return true;
}
}
}
@Override
public void afterStep(StepResult result, ScenarioRuntime context) {
if (result.getResult().isFailed()) {
String errorMessage = result.getErrorMessage();
handler.output("*** step failed: " + errorMessage + "\n");
stop("exception", errorMessage);
errored = true;
}
pushDebugFrameVariables(context);
}
private void pushDebugFrameVariables(ScenarioRuntime context) {
Map vars = context.engine.vars.entrySet().stream()
.collect(Collectors.toMap(v -> v.getKey(), v -> v.getValue().copy(true)));
Stack