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

LiveEdit: breakpoints updates and fixes for related problems

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

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@4533 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent ff0775c3
......@@ -31,7 +31,6 @@
#include "codegen-inl.h"
#include "compiler.h"
#include "debug.h"
#include "liveedit.h"
#include "oprofile-agent.h"
#include "prettyprinter.h"
#include "register-allocator-inl.h"
......@@ -204,7 +203,6 @@ Handle<Code> CodeGenerator::MakeCodeEpilogue(MacroAssembler* masm,
// all the pieces into a Code object. This function is only to be called by
// the compiler.cc code.
Handle<Code> CodeGenerator::MakeCode(CompilationInfo* info) {
LiveEditFunctionTracker live_edit_tracker(info->function());
Handle<Script> script = info->script();
if (!script->IsUndefined() && !script->source()->IsUndefined()) {
int len = String::cast(script->source())->length();
......@@ -216,7 +214,6 @@ Handle<Code> CodeGenerator::MakeCode(CompilationInfo* info) {
MacroAssembler masm(NULL, kInitialBufferSize);
CodeGenerator cgen(&masm);
CodeGeneratorScope scope(&cgen);
live_edit_tracker.RecordFunctionScope(info->function()->scope());
cgen.Generate(info);
if (cgen.HasStackOverflow()) {
ASSERT(!Top::has_pending_exception());
......@@ -225,9 +222,7 @@ Handle<Code> CodeGenerator::MakeCode(CompilationInfo* info) {
InLoopFlag in_loop = (cgen.loop_nesting() != 0) ? IN_LOOP : NOT_IN_LOOP;
Code::Flags flags = Code::ComputeFlags(Code::FUNCTION, in_loop);
Handle<Code> result = MakeCodeEpilogue(cgen.masm(), flags, info);
live_edit_tracker.RecordFunctionCode(result);
return result;
return MakeCodeEpilogue(cgen.masm(), flags, info);
}
......
......@@ -192,6 +192,8 @@ static Handle<SharedFunctionInfo> MakeFunctionInfo(bool is_global,
FunctionLiteral* lit =
MakeAST(is_global, script, extension, pre_data, is_json);
LiveEditFunctionTracker live_edit_tracker(lit);
// Check for parse errors.
if (lit == NULL) {
ASSERT(Top::has_pending_exception());
......@@ -253,6 +255,8 @@ static Handle<SharedFunctionInfo> MakeFunctionInfo(bool is_global,
Debugger::OnAfterCompile(script, Debugger::NO_AFTER_COMPILE_FLAGS);
#endif
live_edit_tracker.RecordFunctionInfo(result, lit);
return result;
}
......@@ -448,6 +452,7 @@ bool Compiler::CompileLazy(CompilationInfo* info) {
Handle<SharedFunctionInfo> Compiler::BuildFunctionInfo(FunctionLiteral* literal,
Handle<Script> script,
AstVisitor* caller) {
LiveEditFunctionTracker live_edit_tracker(literal);
#ifdef DEBUG
// We should not try to compile the same function literal more than
// once.
......@@ -552,6 +557,7 @@ Handle<SharedFunctionInfo> Compiler::BuildFunctionInfo(FunctionLiteral* literal,
// the resulting function.
SetExpectedNofPropertiesFromEstimate(result,
literal->expected_property_count());
live_edit_tracker.RecordFunctionInfo(result, literal);
return result;
}
......
......@@ -124,12 +124,6 @@ BreakPoint.prototype.source_position = function() {
};
BreakPoint.prototype.updateSourcePosition = function(new_position, script) {
this.source_position_ = new_position;
// TODO(635): also update line and column.
};
BreakPoint.prototype.hit_count = function() {
return this.hit_count_;
};
......@@ -245,6 +239,21 @@ function ScriptBreakPoint(type, script_id_or_name, opt_line, opt_column,
}
//Creates a clone of script breakpoint that is linked to another script.
ScriptBreakPoint.prototype.cloneForOtherScript = function (other_script) {
var copy = new ScriptBreakPoint(Debug.ScriptBreakPointType.ScriptId,
other_script.id, this.line_, this.column_, this.groupId_);
copy.number_ = next_break_point_number++;
script_break_points.push(copy);
copy.hit_count_ = this.hit_count_;
copy.active_ = this.active_;
copy.condition_ = this.condition_;
copy.ignoreCount_ = this.ignoreCount_;
return copy;
}
ScriptBreakPoint.prototype.number = function() {
return this.number_;
};
......@@ -280,6 +289,13 @@ ScriptBreakPoint.prototype.column = function() {
};
ScriptBreakPoint.prototype.update_positions = function(line, column) {
this.line_ = line;
this.column_ = column;
}
ScriptBreakPoint.prototype.hit_count = function() {
return this.hit_count_;
};
......@@ -406,6 +422,17 @@ function UpdateScriptBreakPoints(script) {
}
function GetScriptBreakPoints(script) {
var result = [];
for (var i = 0; i < script_break_points.length; i++) {
if (script_break_points[i].matchesScript(script)) {
result.push(script_break_points[i]);
}
}
return result;
}
Debug.setListener = function(listener, opt_data) {
if (!IS_FUNCTION(listener) && !IS_UNDEFINED(listener) && !IS_NULL(listener)) {
throw new Error('Parameters have wrong types.');
......
......@@ -450,7 +450,6 @@ Handle<Code> FullCodeGenerator::MakeCode(CompilationInfo* info) {
CodeGenerator::MakeCodePrologue(info);
const int kInitialBufferSize = 4 * KB;
MacroAssembler masm(NULL, kInitialBufferSize);
LiveEditFunctionTracker live_edit_tracker(info->function());
FullCodeGenerator cgen(&masm);
cgen.Generate(info, PRIMARY);
......@@ -459,9 +458,7 @@ Handle<Code> FullCodeGenerator::MakeCode(CompilationInfo* info) {
return Handle<Code>::null();
}
Code::Flags flags = Code::ComputeFlags(Code::FUNCTION, NOT_IN_LOOP);
Handle<Code> result = CodeGenerator::MakeCodeEpilogue(&masm, flags, info);
live_edit_tracker.RecordFunctionCode(result);
return result;
return CodeGenerator::MakeCodeEpilogue(&masm, flags, info);
}
......
This diff is collapsed.
This diff is collapsed.
......@@ -67,8 +67,9 @@ class LiveEditFunctionTracker {
public:
explicit LiveEditFunctionTracker(FunctionLiteral* fun);
~LiveEditFunctionTracker();
void RecordFunctionCode(Handle<Code> code);
void RecordFunctionScope(Scope* scope);
void RecordFunctionInfo(Handle<SharedFunctionInfo> info,
FunctionLiteral* lit);
void RecordRootFunctionInfo(Handle<Code> code);
static bool IsActive();
};
......@@ -85,14 +86,26 @@ class LiveEdit : AllStatic {
static void ReplaceFunctionCode(Handle<JSArray> new_compile_info_array,
Handle<JSArray> shared_info_array);
static void RelinkFunctionToScript(Handle<JSArray> shared_info_array,
Handle<Script> script_handle);
// Updates script field in FunctionSharedInfo.
static void SetFunctionScript(Handle<JSValue> function_wrapper,
Handle<Object> script_handle);
// Returns an array of pairs (new source position, breakpoint_object/array)
// so that JS side could update positions in breakpoint objects.
static Handle<JSArray> PatchFunctionPositions(
static void PatchFunctionPositions(
Handle<JSArray> shared_info_array, Handle<JSArray> position_change_array);
// For a script updates its source field. If old_script_name is provided
// (i.e. is a String), also creates a copy of the script with its original
// source and sends notification to debugger.
static Object* ChangeScriptSource(Handle<Script> original_script,
Handle<String> new_source,
Handle<Object> old_script_name);
// In a code of a parent function replaces original function as embedded
// object with a substitution one.
static void ReplaceRefToNestedFunction(Handle<JSValue> parent_function_shared,
Handle<JSValue> orig_function_shared,
Handle<JSValue> subst_function_shared);
// Checks listed functions on stack and return array with corresponding
// FunctionPatchabilityStatus statuses; extra array element may
// contain general error message. Modifies the current stack and
......
......@@ -9674,38 +9674,30 @@ static Object* Runtime_LiveEditGatherCompileInfo(Arguments args) {
return result;
}
// Changes the source of the script to a new_source and creates a new
// script representing the old version of the script source.
// Changes the source of the script to a new_source.
// If old_script_name is provided (i.e. is a String), also creates a copy of
// the script with its original source and sends notification to debugger.
static Object* Runtime_LiveEditReplaceScript(Arguments args) {
ASSERT(args.length() == 3);
HandleScope scope;
CONVERT_CHECKED(JSValue, original_script_value, args[0]);
CONVERT_ARG_CHECKED(String, new_source, 1);
CONVERT_ARG_CHECKED(String, old_script_name, 2);
Handle<Script> original_script =
Handle<Script>(Script::cast(original_script_value->value()));
Handle<Object> old_script_name(args[2]);
Handle<String> original_source(String::cast(original_script->source()));
CONVERT_CHECKED(Script, original_script_pointer,
original_script_value->value());
Handle<Script> original_script(original_script_pointer);
original_script->set_source(*new_source);
Handle<Script> old_script = Factory::NewScript(original_source);
old_script->set_name(*old_script_name);
old_script->set_line_offset(original_script->line_offset());
old_script->set_column_offset(original_script->column_offset());
old_script->set_data(original_script->data());
old_script->set_type(original_script->type());
old_script->set_context_data(original_script->context_data());
old_script->set_compilation_type(original_script->compilation_type());
old_script->set_eval_from_shared(original_script->eval_from_shared());
old_script->set_eval_from_instructions_offset(
original_script->eval_from_instructions_offset());
Object* old_script = LiveEdit::ChangeScriptSource(original_script,
new_source,
old_script_name);
// Drop line ends so that they will be recalculated.
original_script->set_line_ends(Heap::undefined_value());
Debugger::OnAfterCompile(old_script, Debugger::SEND_WHEN_DEBUGGING);
return *(GetScriptWrapper(old_script));
if (old_script->IsScript()) {
Handle<Script> script_handle(Script::cast(old_script));
return *(GetScriptWrapper(script_handle));
} else {
return Heap::null_value();
}
}
// Replaces code of SharedFunctionInfo with a new one.
......@@ -9721,35 +9713,60 @@ static Object* Runtime_LiveEditReplaceFunctionCode(Arguments args) {
}
// Connects SharedFunctionInfo to another script.
static Object* Runtime_LiveEditRelinkFunctionToScript(Arguments args) {
static Object* Runtime_LiveEditFunctionSetScript(Arguments args) {
ASSERT(args.length() == 2);
HandleScope scope;
CONVERT_ARG_CHECKED(JSArray, shared_info_array, 0);
CONVERT_ARG_CHECKED(JSValue, script_value, 1);
Handle<Script> script = Handle<Script>(Script::cast(script_value->value()));
Handle<Object> function_object(args[0]);
Handle<Object> script_object(args[1]);
LiveEdit::RelinkFunctionToScript(shared_info_array, script);
if (function_object->IsJSValue()) {
Handle<JSValue> function_wrapper = Handle<JSValue>::cast(function_object);
if (script_object->IsJSValue()) {
CONVERT_CHECKED(Script, script, JSValue::cast(*script_object)->value());
script_object = Handle<Object>(script);
}
LiveEdit::SetFunctionScript(function_wrapper, script_object);
} else {
// Just ignore this. We may not have a SharedFunctionInfo for some functions
// and we check it in this function.
}
return Heap::undefined_value();
}
// In a code of a parent function replaces original function as embedded object
// with a substitution one.
static Object* Runtime_LiveEditReplaceRefToNestedFunction(Arguments args) {
ASSERT(args.length() == 3);
HandleScope scope;
CONVERT_ARG_CHECKED(JSValue, parent_wrapper, 0);
CONVERT_ARG_CHECKED(JSValue, orig_wrapper, 1);
CONVERT_ARG_CHECKED(JSValue, subst_wrapper, 2);
LiveEdit::ReplaceRefToNestedFunction(parent_wrapper, orig_wrapper,
subst_wrapper);
return Heap::undefined_value();
}
// Updates positions of a shared function info (first parameter) according
// to script source change. Text change is described in second parameter as
// array of groups of 3 numbers:
// (change_begin, change_end, change_end_new_position).
// Each group describes a change in text; groups are sorted by change_begin.
// Returns an array of pairs (new source position, breakpoint_object/array)
// so that JS side could update positions in breakpoint objects.
static Object* Runtime_LiveEditPatchFunctionPositions(Arguments args) {
ASSERT(args.length() == 2);
HandleScope scope;
CONVERT_ARG_CHECKED(JSArray, shared_array, 0);
CONVERT_ARG_CHECKED(JSArray, position_change_array, 1);
Handle<Object> result =
LiveEdit::PatchFunctionPositions(shared_array, position_change_array);
LiveEdit::PatchFunctionPositions(shared_array, position_change_array);
return *result;
return Heap::undefined_value();
}
......
......@@ -337,7 +337,8 @@ namespace internal {
F(LiveEditGatherCompileInfo, 2, 1) \
F(LiveEditReplaceScript, 3, 1) \
F(LiveEditReplaceFunctionCode, 2, 1) \
F(LiveEditRelinkFunctionToScript, 2, 1) \
F(LiveEditFunctionSetScript, 2, 1) \
F(LiveEditReplaceRefToNestedFunction, 3, 1) \
F(LiveEditPatchFunctionPositions, 2, 1) \
F(LiveEditCheckAndDropActivations, 2, 1) \
F(LiveEditCompareStringsLinewise, 2, 1) \
......
// Copyright 2010 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Flags: --expose-debug-as debug
// Get the Debug object exposed from the debug context global object.
// In this test case we edit a script so that techincally function text
// hasen't been changed. However actually function became one level more nested
// and must be recompiled because it uses variable from outer scope.
Debug = debug.Debug
var function_z_text =
" function Z() {\n"
+ " return 2 + p;\n"
+ " }\n";
eval(
"function Factory(p) {\n"
+ "return (\n"
+ function_z_text
+ ");\n"
+ "}\n"
);
var z6 = Factory(6);
assertEquals(8, z6());
var script = Debug.findScript(Factory);
var new_source = script.source.replace(function_z_text, "function Intermediate() {\nreturn (\n" + function_z_text + ")\n;\n}\n");
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(8, z6());
var z100 = Factory(100)();
assertEquals(102, z100());
// Copyright 2010 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Flags: --expose-debug-as debug
// Get the Debug object exposed from the debug context global object.
Debug = debug.Debug
var function_z_text =
" function Z() {\n"
+ " return 'Z';\n" // Breakpoint line ( #6 )
+ " }\n";
eval(
"function F25() {\n"
+ " return 25;\n" // Breakpoint line ( #1 )
+ "}\n"
+ "function F26 () {\n"
+ " var x = 20;\n"
+ function_z_text // function takes exactly 3 lines
// // Breakpoint line ( #6 )
//
+ " var y = 6;\n"
+ " return x + y;\n"
+ "}\n"
+ "function Nested() {\n"
+ " var a = 30;\n"
+ " return function F27() {\n"
+ " var b = 3;\n" // Breakpoint line ( #14 )
+ " return a - b;\n"
+ " }\n"
+ "}\n"
);
assertEquals(25, F25());
assertEquals(26, F26());
var script = Debug.findScript(F25);
Debug.setScriptBreakPoint(Debug.ScriptBreakPointType.ScriptId, script.id, 1, 1, "true || false || false");
Debug.setScriptBreakPoint(Debug.ScriptBreakPointType.ScriptId, script.id, 6, 1, "true || false || false");
Debug.setScriptBreakPoint(Debug.ScriptBreakPointType.ScriptId, script.id, 14, 1, "true || false || false");
assertEquals(3, Debug.scriptBreakPoints().length);
var new_source = script.source.replace(function_z_text, "");
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");
var breaks = Debug.scriptBreakPoints();
// One breakpoint gets duplicated in a old version of script.
assertTrue(breaks.length > 3 + 1);
var breakpoints_in_script = 0;
var break_position_map = {};
for (var i = 0; i < breaks.length; i++) {
if (breaks[i].script_id() == script.id) {
break_position_map[breaks[i].line()] = true;
breakpoints_in_script++;
}
}
assertEquals(3, breakpoints_in_script);
// Check 2 breakpoints. The one in deleted function should have been moved somewhere.
assertTrue(break_position_map[1]);
assertTrue(break_position_map[11]);
......@@ -31,7 +31,7 @@
Debug = debug.Debug
function CheckCompareOneWay(s1, s2) {
var diff_array = Debug.LiveEdit.CompareStringsLinewise(s1, s2);
var diff_array = Debug.LiveEdit.TestApi.CompareStringsLinewise(s1, s2);
var pos1 = 0;
var pos2 = 0;
......
// Copyright 2010 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Flags: --expose-debug-as debug
// Get the Debug object exposed from the debug context global object.
Debug = debug.Debug
function Return2010() {
return 2010;
}
// Diff it trivial: zero chunks
var NoChunkTranslator = new Debug.LiveEdit.TestApi.PosTranslator([]);
assertEquals(0, NoChunkTranslator.Translate(0));
assertEquals(10, NoChunkTranslator.Translate(10));
// Diff has one chunk
var SingleChunkTranslator = new Debug.LiveEdit.TestApi.PosTranslator([20, 30, 25]);
assertEquals(0, SingleChunkTranslator.Translate(0));
assertEquals(5, SingleChunkTranslator.Translate(5));
assertEquals(10, SingleChunkTranslator.Translate(10));
assertEquals(19, SingleChunkTranslator.Translate(19));
assertEquals(2010, SingleChunkTranslator.Translate(20, Return2010));
assertEquals(25, SingleChunkTranslator.Translate(30));
assertEquals(26, SingleChunkTranslator.Translate(31));
assertEquals(2010, SingleChunkTranslator.Translate(26, Return2010));
try {
SingleChunkTranslator.Translate(21);
assertTrue(false);
} catch (ignore) {
}
try {
SingleChunkTranslator.Translate(24);
assertTrue(false);
} catch (ignore) {
}
// Diff has several chunk (3). See the table below.
/*
chunks: (new <- old)
10 10
15 20
35 40
50 40
70 60
70 70
*/
var MultiChunkTranslator = new Debug.LiveEdit.TestApi.PosTranslator([10, 20, 15, 40, 40, 50, 60, 70, 70 ]);
assertEquals(5, MultiChunkTranslator.Translate(5));
assertEquals(9, MultiChunkTranslator.Translate(9));
assertEquals(2010, MultiChunkTranslator.Translate(10, Return2010));
assertEquals(15, MultiChunkTranslator.Translate(20));
assertEquals(20, MultiChunkTranslator.Translate(25));
assertEquals(34, MultiChunkTranslator.Translate(39));
assertEquals(50, MultiChunkTranslator.Translate(40, Return2010));
assertEquals(55, MultiChunkTranslator.Translate(45));
assertEquals(69, MultiChunkTranslator.Translate(59));
assertEquals(2010, MultiChunkTranslator.Translate(60, Return2010));
assertEquals(70, MultiChunkTranslator.Translate(70));
assertEquals(75, MultiChunkTranslator.Translate(75));
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