Commit af63e616 authored by peter.rybin@gmail.com's avatar peter.rybin@gmail.com

Support multi-chunk differences

Review URL: http://codereview.chromium.org/1672006

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@4467 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent e44869ae
......@@ -28,26 +28,52 @@
// LiveEdit feature implementation. The script should be executed after
// debug-debugger.js.
// A LiveEdit namespace is declared inside a single function constructor.
// A LiveEdit namespace. It contains functions that modifies JavaScript code
// according to changes of script source (if possible).
//
// When new script source is put in, the difference is calculated textually,
// in form of list of delete/add/change chunks. The functions that include
// change chunk(s) get recompiled, or their enclosing functions are
// recompiled instead.
// If the function may not be recompiled (e.g. it was completely erased in new
// version of the script) it remains unchanged, but the code that could
// create a new instance of this function goes away. An old version of script
// is created to back up this obsolete function.
// All unchanged functions have their positions updated accordingly.
//
// LiveEdit namespace is declared inside a single function constructor.
Debug.LiveEdit = new function() {
// Changes script text and recompiles all relevant functions if possible.
// Applies the change to the script.
// The change is always a substring (change_pos, change_pos + change_len)
// being replaced with a completely different string new_str.
//
// Only one function will have its Code changed in result of this function.
// All nested functions (should they have any instances at the moment) are
// left unchanged and re-linked to a newly created script instance
// representing old version of the source. (Generally speaking,
// during the change all nested functions are erased and completely different
// set of nested functions are introduced.) All other functions just have
// their positions updated.
// This API is a legacy and is obsolete.
//
// @param {Script} script that is being changed
// @param {Array} change_log a list that collects engineer-readable
// description of what happened.
function ApplyPatch(script, change_pos, change_len, new_str,
change_log) {
var old_source = script.source;
// Prepare new source string.
var new_source = old_source.substring(0, change_pos) +
new_str + old_source.substring(change_pos + change_len);
return ApplyPatchMultiChunk(script,
[ change_pos, change_pos + change_len, change_pos + new_str.length],
new_source, change_log);
}
// Function is public.
this.ApplyPatch = ApplyPatch;
// Forward declaration for minifier.
var FunctionStatus;
// Applies the change to the script.
// The change is in form of list of chunks encoded in a single array as
// a series of triplets (pos1_start, pos1_end, pos2_end)
function ApplyPatchMultiChunk(script, diff_array, new_source, change_log) {
// Fully compiles source string as a script. Returns Array of
// FunctionCompileInfo -- a descriptions of all functions of the script.
......@@ -117,27 +143,6 @@ Debug.LiveEdit = new function() {
return compile_info;
}
// Given a positions, finds a function that fully includes the entire
// change.
function FindChangedFunction(compile_info, offset, len) {
// First condition: function should start before the change region.
// Function #0 (whole-script function) always does, but we want
// one, that is later in this list.
var index = 0;
while (index + 1 < compile_info.length &&
compile_info[index + 1].start_position <= offset) {
index++;
}
// Now we are at the last function that begins before the change
// region. The function that covers entire change region is either
// this function or the enclosing one.
for (; compile_info[index].end_position < offset + len;
index = compile_info[index].outer_index) {
Assert(index != -1);
}
return index;
}
// Variable forward declarations. Preprocessor "Minifier" needs them.
var old_compile_info;
var shared_infos;
......@@ -156,34 +161,27 @@ Debug.LiveEdit = new function() {
// Replaces function's Code.
function PatchCode(new_info, shared_info) {
if (shared_info) {
%LiveEditReplaceFunctionCode(new_info.raw_array, shared_info.raw_array);
change_log.push( {function_patched: new_info.function_name} );
} else {
change_log.push( {function_patched: new_info.function_name,
function_info_not_found: true} );
}
var change_len_old;
var change_len_new;
// Translate position in old version of script into position in new
// version of script.
function PosTranslator(old_pos) {
if (old_pos <= change_pos) {
return old_pos;
}
if (old_pos >= change_pos + change_len_old) {
return old_pos + change_len_new - change_len_old;
}
return -1;
}
var position_change_array;
var position_patch_report;
function PatchPositions(new_info, shared_info) {
function PatchPositions(old_info, shared_info) {
if (!shared_info) {
// TODO(LiveEdit): explain what is happening.
// TODO(LiveEdit): function is not compiled yet or is already collected.
position_patch_report.push(
{ name: old_info.function_name, info_not_found: true } );
return;
}
var breakpoint_position_update = %LiveEditPatchFunctionPositions(
shared_info.raw_array, position_change_array);
shared_info.raw_array, diff_array);
for (var i = 0; i < breakpoint_position_update.length; i += 2) {
var new_pos = breakpoint_position_update[i];
var break_point_object = breakpoint_position_update[i + 1];
......@@ -191,7 +189,7 @@ Debug.LiveEdit = new function() {
{ from: break_point_object.source_position(), to: new_pos } } );
break_point_object.updateSourcePosition(new_pos, script);
}
position_patch_report.push( { name: new_info.function_name } );
position_patch_report.push( { name: old_info.function_name } );
}
var link_to_old_script_report;
......@@ -199,21 +197,18 @@ Debug.LiveEdit = new function() {
// Makes a function associated with another instance of a script (the
// one representing its old version). This way the function still
// may access its own text.
function LinkToOldScript(shared_info) {
function LinkToOldScript(shared_info, old_info_node) {
if (shared_info) {
%LiveEditRelinkFunctionToScript(shared_info.raw_array, old_script);
link_to_old_script_report.push( { name: shared_info.function_name } );
} else {
link_to_old_script_report.push(
{ name: old_info_node.info.function_name, not_found: true } );
}
}
var old_source = script.source;
var change_len_old = change_len;
var change_len_new = new_str.length;
// Prepare new source string.
var new_source = old_source.substring(0, change_pos) +
new_str + old_source.substring(change_pos + change_len);
// Find all SharedFunctionInfo's that are compiled from this script.
var shared_raw_list = %LiveEditFindSharedFunctionInfosForScript(script);
......@@ -235,37 +230,65 @@ Debug.LiveEdit = new function() {
throw new Failure("Failed to compile new version of script: " + e);
}
// An index of a single function, that is going to have its code replaced.
var function_being_patched =
FindChangedFunction(old_compile_info, change_pos, change_len_old);
var pos_translator = new PosTranslator(diff_array);
// Build tree structures for old and new versions of the script.
var root_old_node = BuildCodeInfoTree(old_compile_info);
var root_new_node = BuildCodeInfoTree(new_compile_info);
// Analyze changes.
MarkChangedFunctions(root_old_node, pos_translator.GetChunks());
FindCorrespondingFunctions(root_old_node, root_new_node);
// Prepare to-do lists.
var replace_code_list = new Array();
var link_to_old_script_list = new Array();
var update_positions_list = new Array();
// In old and new script versions function with a change should have the
// same indexes.
var function_being_patched2 =
FindChangedFunction(new_compile_info, change_pos, change_len_new);
Assert(function_being_patched == function_being_patched2,
"inconsistent old/new compile info");
function HarvestTodo(old_node) {
function CollectDamaged(node) {
link_to_old_script_list.push(node);
for (var i = 0; i < node.children.length; i++) {
CollectDamaged(node.children[i]);
}
}
if (old_node.status == FunctionStatus.DAMAGED) {
CollectDamaged(old_node);
return;
}
if (old_node.status == FunctionStatus.UNCHANGED) {
update_positions_list.push(old_node);
} else if (old_node.status == FunctionStatus.SOURCE_CHANGED) {
update_positions_list.push(old_node);
} else if (old_node.status == FunctionStatus.CHANGED) {
replace_code_list.push(old_node);
}
for (var i = 0; i < old_node.children.length; i++) {
HarvestTodo(old_node.children[i]);
}
}
// Check that function being patched has the same expectations in a new
// version. Otherwise we cannot safely patch its behavior and should
// choose the outer function instead.
while (!CompareFunctionExpectations(
old_compile_info[function_being_patched],
new_compile_info[function_being_patched])) {
HarvestTodo(root_old_node);
Assert(old_compile_info[function_being_patched].outer_index ==
new_compile_info[function_being_patched].outer_index);
function_being_patched =
old_compile_info[function_being_patched].outer_index;
Assert(function_being_patched != -1);
// Collect shared infos for functions whose code need to be patched.
var replaced_function_infos = new Array();
for (var i = 0; i < replace_code_list.length; i++) {
var info = FindFunctionInfo(replace_code_list[i].array_index);
if (info) {
replaced_function_infos.push(info);
}
}
// Check that function being patched is not currently on stack.
CheckStackActivations(
[ FindFunctionInfo(function_being_patched) ], change_log );
CheckStackActivations(replaced_function_infos, change_log);
// We haven't changed anything before this line yet.
// Committing all changes.
// Create old script if there are function linked to old version.
if (link_to_old_script_list.length > 0) {
var old_script_name = CreateNameForOldScript(script);
// Update the script text and create a new script representing an old
......@@ -273,55 +296,36 @@ Debug.LiveEdit = new function() {
var old_script = %LiveEditReplaceScript(script, new_source,
old_script_name);
PatchCode(new_compile_info[function_being_patched],
FindFunctionInfo(function_being_patched));
var link_to_old_script_report = new Array();
change_log.push( { linked_to_old_script: link_to_old_script_report } );
var position_patch_report = new Array();
change_log.push( {position_patched: position_patch_report} );
// We need to link to old script all former nested functions.
for (var i = 0; i < link_to_old_script_list.length; i++) {
LinkToOldScript(
FindFunctionInfo(link_to_old_script_list[i].array_index),
link_to_old_script_list[i]);
}
}
var position_change_array = [ change_pos,
change_pos + change_len_old,
change_pos + change_len_new ];
// Update positions of all outer functions (i.e. all functions, that
// are partially below the function being patched).
for (var i = new_compile_info[function_being_patched].outer_index;
i != -1;
i = new_compile_info[i].outer_index) {
PatchPositions(new_compile_info[i], FindFunctionInfo(i));
}
// Update positions of all functions that are fully below the function
// being patched.
var old_next_sibling =
old_compile_info[function_being_patched].next_sibling_index;
var new_next_sibling =
new_compile_info[function_being_patched].next_sibling_index;
// We simply go over the tail of both old and new lists. Their tails should
// have an identical structure.
if (old_next_sibling == -1) {
Assert(new_next_sibling == -1);
} else {
Assert(old_compile_info.length - old_next_sibling ==
new_compile_info.length - new_next_sibling);
for (var i = old_next_sibling, j = new_next_sibling;
i < old_compile_info.length; i++, j++) {
PatchPositions(new_compile_info[j], FindFunctionInfo(i));
}
for (var i = 0; i < replace_code_list.length; i++) {
PatchCode(replace_code_list[i].corresponding_node.info,
FindFunctionInfo(replace_code_list[i].array_index));
}
var link_to_old_script_report = new Array();
change_log.push( { linked_to_old_script: link_to_old_script_report } );
var position_patch_report = new Array();
change_log.push( {position_patched: position_patch_report} );
// We need to link to old script all former nested functions.
for (var i = function_being_patched + 1; i < old_next_sibling; i++) {
LinkToOldScript(FindFunctionInfo(i), old_script);
for (var i = 0; i < update_positions_list.length; i++) {
// TODO(LiveEdit): take into account wether it's source_changed or
// unchanged and whether positions changed at all.
PatchPositions(update_positions_list[i].info,
FindFunctionInfo(update_positions_list[i].array_index));
}
}
// Function is public.
this.ApplyPatch = ApplyPatch;
this.ApplyPatchMultiChunk = ApplyPatchMultiChunk;
function Assert(condition, message) {
if (!condition) {
......@@ -333,6 +337,296 @@ Debug.LiveEdit = new function() {
}
}
function DiffChunk(pos1, pos2, len1, len2) {
this.pos1 = pos1;
this.pos2 = pos2;
this.len1 = len1;
this.len2 = len2;
}
function PosTranslator(diff_array) {
var chunks = new Array();
var pos1 = 0;
var pos2 = 0;
for (var i = 0; i < diff_array.length; i += 3) {
pos2 += diff_array[i] - pos1 + pos2;
pos1 = diff_array[i];
chunks.push(new DiffChunk(pos1, pos2, diff_array[i + 1] - pos1,
diff_array[i + 2] - pos2));
pos1 = diff_array[i + 1];
pos2 = diff_array[i + 2];
}
this.chunks = chunks;
}
PosTranslator.prototype.GetChunks = function() {
return this.chunks;
}
PosTranslator.prototype.Translate = function(pos, inside_chunk_handler) {
var array = this.chunks;
if (array.length == 0 || pos < array[0]) {
return pos;
}
var chunk_index1 = 0;
var chunk_index2 = array.length - 1;
while (chunk_index1 < chunk_index2) {
var middle_index = (chunk_index1 + chunk_index2) / 2;
if (pos < array[middle_index + 1].pos1) {
chunk_index2 = middle_index;
} else {
chunk_index1 = middle_index + 1;
}
}
var chunk = array[chunk_index1];
if (pos >= chunk.pos1 + chunk.len1) {
return pos += chunk.pos2 + chunk.len2 - chunk.pos1 - chunk.len1;
}
if (!inside_chunk_handler) {
inside_chunk_handler = PosTranslator.default_inside_chunk_handler;
}
inside_chunk_handler(pos, chunk);
}
PosTranslator.default_inside_chunk_handler = function() {
Assert(false, "Cannot translate position in chaged area");
}
var FunctionStatus = {
// No change to function or its inner functions; however its positions
// in script may have been shifted.
UNCHANGED: "unchanged",
// The code of a function remains unchanged, but something happened inside
// some inner functions.
SOURCE_CHANGED: "source changed",
// The code of a function is changed or some nested function cannot be
// properly patched so this function must be recompiled.
CHANGED: "changed",
// Function is changed but cannot be patched.
DAMAGED: "damaged"
}
function CodeInfoTreeNode(code_info, children, array_index) {
this.info = code_info;
this.children = children;
// an index in array of compile_info
this.array_index = array_index;
this.parent = void(0);
this.status = FunctionStatus.UNCHANGED;
// Status explanation is used for debugging purposes and will be shown
// in user UI if some explanations are needed.
this.status_explanation = void(0);
this.new_start_pos = void(0);
this.new_end_pos = void(0);
this.corresponding_node = void(0);
}
// From array of function infos that is implicitly a tree creates
// an actual tree of functions in script.
function BuildCodeInfoTree(code_info_array) {
// Throughtout all function we iterate over input array.
var index = 0;
// Recursive function that builds a branch of tree.
function BuildNode() {
var my_index = index;
index++;
var child_array = new Array();
while (index < code_info_array.length &&
code_info_array[index].outer_index == my_index) {
child_array.push(BuildNode());
}
var node = new CodeInfoTreeNode(code_info_array[my_index], child_array,
my_index);
for (var i = 0; i < child_array.length; i++) {
child_array[i].parent = node;
}
return node;
}
var root = BuildNode();
Assert(index == code_info_array.length);
return root;
}
// Applies a list of the textual diff chunks onto the tree of functions.
// Determines status of each function (from unchanged to damaged). However
// children of unchanged functions are ignored.
function MarkChangedFunctions(code_info_tree, chunks) {
// A convenient interator over diff chunks that also translates
// positions from old to new in a current non-changed part of script.
var chunk_it = new function() {
var chunk_index = 0;
var pos_diff = 0;
this.current = function() { return chunks[chunk_index]; }
this.next = function() {
var chunk = chunks[chunk_index];
pos_diff = chunk.pos2 + chunk.len2 - (chunk.pos1 + chunk.len1);
chunk_index++;
}
this.done = function() { return chunk_index >= chunks.length; }
this.TranslatePos = function(pos) { return pos + pos_diff; }
};
// A recursive function that processes internals of a function and all its
// inner functions. Iterator chunk_it initially points to a chunk that is
// below function start.
function ProcessInternals(info_node) {
info_node.new_start_pos = chunk_it.TranslatePos(
info_node.info.start_position);
var child_index = 0;
var code_changed = false;
var source_changed = false;
// Simultaneously iterates over child functions and over chunks.
while (!chunk_it.done() &&
chunk_it.current().pos1 < info_node.info.end_position) {
if (child_index < info_node.children.length) {
var child = info_node.children[child_index];
if (child.info.end_position <= chunk_it.current().pos1) {
ProcessUnchangedChild(child);
child_index++;
continue;
} else if (child.info.start_position >=
chunk_it.current().pos1 + chunk_it.current().len1) {
code_changed = true;
chunk_it.next();
continue;
} else if (child.info.start_position <= chunk_it.current().pos1 &&
child.info.end_position >= chunk_it.current().pos1 +
chunk_it.current().len1) {
ProcessInternals(child);
source_changed = source_changed ||
( child.status != FunctionStatus.UNCHANGED );
code_changed = code_changed ||
( child.status == FunctionStatus.DAMAGED );
child_index++;
continue;
} else {
code_changed = true;
child.status = FunctionStatus.DAMAGED;
child.status_explanation =
"Text diff overlaps with function boundary";
child_index++;
continue;
}
} else {
if (chunk_it.current().pos1 + chunk_it.current().len1 <=
info_node.info.end_position) {
info_node.status = FunctionStatus.CHANGED;
chunk_it.next();
continue;
} else {
info_node.status = FunctionStatus.DAMAGED;
info_node.status_explanation =
"Text diff overlaps with function boundary";
return;
}
}
Assert("Unreachable", false);
}
while (child_index < info_node.children.length) {
var child = info_node.children[child_index];
ProcessUnchangedChild(child);
child_index++;
}
if (code_changed) {
info_node.status = FunctionStatus.CHANGED;
} else if (source_changed) {
info_node.status = FunctionStatus.SOURCE_CHANGED;
}
info_node.new_end_pos =
chunk_it.TranslatePos(info_node.info.end_position);
}
function ProcessUnchangedChild(node) {
node.new_start_pos = chunk_it.TranslatePos(node.info.start_position);
node.new_end_pos = chunk_it.TranslatePos(node.info.end_position);
}
ProcessInternals(code_info_tree);
}
// For ecah old function (if it is not damaged) tries to find a corresponding
// function in new script. Typically it should succeed (non-damaged functions
// by definition may only have changes inside their bodies). However there are
// reasons for corresponence not to be found; function with unmodified text
// in new script may become enclosed into other function; the innocent change
// inside function body may in fact be something like "} function B() {" that
// splits a function into 2 functions.
function FindCorrespondingFunctions(old_code_tree, new_code_tree) {
// A recursive function that tries to find a correspondence for all
// child functions and for their inner functions.
function ProcessChildren(old_node, new_node) {
var old_children = old_node.children;
var new_children = new_node.children;
var old_index = 0;
var new_index = 0;
while (old_index < old_children.length) {
if (old_children[old_index].status == FunctionStatus.DAMAGED) {
old_index++;
} else if (new_index < new_children.length) {
if (new_children[new_index].info.start_position <
old_children[old_index].new_start_pos) {
new_index++;
} else if (new_children[new_index].info.start_position ==
old_children[old_index].new_start_pos) {
if (new_children[new_index].info.end_position ==
old_children[old_index].new_end_pos) {
old_children[old_index].corresponding_node =
new_children[new_index];
if (old_children[old_index].status != FunctionStatus.UNCHANGED) {
ProcessChildren(old_children[old_index],
new_children[new_index]);
if (old_children[old_index].status == FunctionStatus.DAMAGED) {
old_node.status = FunctionStatus.CHANGED;
}
}
} else {
old_children[old_index].status = FunctionStatus.DAMAGED;
old_children[old_index].status_explanation =
"No corresponding function in new script found";
old_node.status = FunctionStatus.CHANGED;
}
new_index++;
old_index++;
} else {
old_children[old_index].status = FunctionStatus.DAMAGED;
old_children[old_index].status_explanation =
"No corresponding function in new script found";
old_node.status = FunctionStatus.CHANGED;
old_index++;
}
} else {
old_children[old_index].status = FunctionStatus.DAMAGED;
old_children[old_index].status_explanation =
"No corresponding function in new script found";
old_node.status = FunctionStatus.CHANGED;
old_index++;
}
}
if (old_node.status == FunctionStatus.CHANGED) {
if (!CompareFunctionExpectations(old_node.info, new_node.info)) {
old_node.status = FunctionStatus.DAMAGED;
old_node.status_explanation = "Changed code expectations";
}
}
}
ProcessChildren(old_code_tree, new_code_tree);
old_code_tree.corresponding_node = new_code_tree;
Assert(old_code_tree.status != FunctionStatus.DAMAGED,
"Script became damaged");
}
// An object describing function compilation details. Its index fields
// apply to indexes inside array that stores these objects.
function FunctionCompileInfo(raw_array) {
......@@ -469,13 +763,12 @@ Debug.LiveEdit = new function() {
// LiveEdit main entry point: changes a script text to a new string.
function SetScriptSource(script, new_source, change_log) {
var old_source = script.source;
var diff = FindSimpleDiff(old_source, new_source);
if (!diff) {
var diff = CompareStringsLinewise(old_source, new_source);
if (diff.length == 0) {
change_log.push( { empty_diff: true } );
return;
}
ApplyPatch(script, diff.change_pos, diff.old_len,
new_source.substring(diff.change_pos, diff.change_pos + diff.new_len),
change_log);
ApplyPatchMultiChunk(script, diff, new_source, change_log);
}
// Function is public.
this.SetScriptSource = SetScriptSource;
......
......@@ -346,7 +346,7 @@ class LineArrayCompareInput : public Compare::Input {
// Stores compare result in JSArray. Each chunk is stored as 3 array elements:
// (pos1, len1, len2).
// (pos1_begin, pos1_end, pos2_end).
class LineArrayCompareOutput : public Compare::Output {
public:
LineArrayCompareOutput(LineEndsWrapper line_ends1, LineEndsWrapper line_ends2)
......@@ -362,9 +362,9 @@ class LineArrayCompareOutput : public Compare::Output {
SetElement(array_, current_size_, Handle<Object>(Smi::FromInt(char_pos1)));
SetElement(array_, current_size_ + 1,
Handle<Object>(Smi::FromInt(char_len1)));
Handle<Object>(Smi::FromInt(char_pos1 + char_len1)));
SetElement(array_, current_size_ + 2,
Handle<Object>(Smi::FromInt(char_len2)));
Handle<Object>(Smi::FromInt(char_pos2 + char_len2)));
current_size_ += 3;
}
......@@ -717,8 +717,8 @@ class ReferenceCollectorVisitor : public ObjectVisitor {
}
void VisitCodeTarget(RelocInfo* rinfo) {
ASSERT(RelocInfo::IsCodeTarget(rinfo->rmode()));
if (Code::GetCodeFromTargetAddress(rinfo->target_address()) == original_) {
if (RelocInfo::IsCodeTarget(rinfo->rmode()) &&
Code::GetCodeFromTargetAddress(rinfo->target_address()) == original_) {
reloc_infos_.Add(*rinfo);
}
}
......
......@@ -111,7 +111,7 @@ class LiveEdit : AllStatic {
};
// Compares 2 strings line-by-line and returns diff in form of array of
// triplets (pos1, len1, len2) describing list of diff chunks.
// triplets (pos1, pos1_end, pos2_end) describing list of diff chunks.
static Handle<JSArray> CompareStringsLinewise(Handle<String> s1,
Handle<String> s2);
};
......
......@@ -9766,7 +9766,7 @@ static Object* Runtime_LiveEditCheckAndDropActivations(Arguments args) {
}
// Compares 2 strings line-by-line and returns diff in form of JSArray of
// triplets (pos1, len1, len2) describing list of diff chunks.
// triplets (pos1, pos1_end, pos2_end) describing list of diff chunks.
static Object* Runtime_LiveEditCompareStringsLinewise(Arguments args) {
ASSERT(args.length() == 2);
HandleScope scope;
......
......@@ -30,18 +30,39 @@
Debug = debug.Debug
eval("var something1 = 25; "
+ " function ChooseAnimal() { return 'Cat'; } "
+ " ChooseAnimal.Helper = function() { return 'Help!'; }");
eval("var something1 = 25; \n"
+ "var something2 = 2010; \n"
+ "function ChooseAnimal() {\n"
+ " return 'Cat';\n"
+ "} \n"
+ "function ChooseFurniture() {\n"
+ " return 'Table';\n"
+ "} \n"
+ "function ChooseNumber() { return 17; } \n"
+ "ChooseAnimal.Factory = function Factory() {\n"
+ " return function FactoryImpl(name) {\n"
+ " return 'Help ' + name;\n"
+ " }\n"
+ "}\n");
assertEquals("Cat", ChooseAnimal());
assertEquals(25, something1);
var script = Debug.findScript(ChooseAnimal);
var new_source = script.source.replace("Cat", "Cap' + 'yb' + 'ara");
var new_source = new_source.replace("25", "26");
var new_source = new_source.replace("Help", "Hello");
var new_source = new_source.replace("17", "18");
print("new source: " + new_source);
var change_log = new Array();
Debug.LiveEdit.SetScriptSource(script, new_source, change_log);
print("Change log: " + JSON.stringify(change_log) + "\n");
assertEquals("Capybara", ChooseAnimal());
// Global variable do not get changed (without restarting script).
assertEquals(25, something1);
// Function is oneliner, so currently it is treated as damaged and not patched.
assertEquals(17, ChooseNumber());
assertEquals("Hello Peter", ChooseAnimal.Factory()("Peter"));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment